CVE-2026-34715
Description
ewe is a Gleam web server. Prior to version 3.0.6, the encode_headers function in src/ewe/internal/encoder.gleam directly interpolates response header keys and values into raw HTTP bytes without validating or stripping CRLF (\r\n) sequences. An application that passes user-controlled data into response headers (e.g., setting a Location redirect header from a request parameter) allows an attacker to inject arbitrary HTTP response content, leading to response splitting, cache poisoning, and possible cross-site scripting. Notably, ewe does validate CRLF in incoming request headers via validate_field_value() in the HTTP/1.1 parser — but provides no equivalent protection for outgoing response headers in the encoder. This issue has been patched in version 3.0.6.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
eweHex | < 3.0.6 | 3.0.6 |
Affected products
1Patches
1ce4ff214d326Merge commit from fork
4 files changed · +107 −43
CHANGELOG.md+6 −0 modified@@ -1,5 +1,11 @@ # Changelog +## Unreleased + +- Sanitize CRLF sequences in outgoing HTTP response headers. +- Eliminate `string.lowercase` in a codebase: validate and lowercase header field names in a single pass, validate and lowercase important protocol header values (like `transfer-encoding`, `connection`, `upgrade` and more) at parse time. +- Include validation of trailer header field names and values during chunked body parsing. + # v3.0.5 - 15.03.2026 - Remove all usage of `string.inspect` as it is an anti-pattern for logging.
src/ewe_ffi.erl+59 −2 modified@@ -1,7 +1,8 @@ -module(ewe_ffi). -export([close_file/1, decode_packet/3, init_clock_storage/0, lookup_http_date/0, now/0, - now_microseconds/0, open_file/1, set_http_date/1, validate_field_value/1, + now_microseconds/0, open_file/1, set_http_date/1, validate_lowercase_field/1, + validate_field_value/1, validate_lowercase_field_value/1, sanitize_header_value/1, coerce_tcp_message/1, parse_path/1]). % Socket @@ -48,12 +49,43 @@ parse_path(Value) -> {ok, {maps:get(path, Uri), Query}} end. +validate_lowercase_field(<<>>) -> + {error, invalid_headers}; +validate_lowercase_field(Value) -> + validate_lowercase_field(Value, <<>>). + +validate_lowercase_field(<<>>, Acc) -> + {ok, Acc}; +validate_lowercase_field(<<C, Rest/bits>>, Acc) when C >= $A, C =< $Z -> + validate_lowercase_field(Rest, <<Acc/binary, (C + 32)>>); +validate_lowercase_field(<<C, Rest/bits>>, Acc) + when C >= $a, C =< $z; + C >= $0, C =< $9; + C =:= $!; + C =:= $#; + C =:= $$; + C =:= $%; + C =:= $&; + C =:= $'; + C =:= $*; + C =:= $+; + C =:= $-; + C =:= $.; + C =:= $^; + C =:= $_; + C =:= $`; + C =:= $|; + C =:= $~ -> + validate_lowercase_field(Rest, <<Acc/binary, C>>); +validate_lowercase_field(_, _) -> + {error, invalid_headers}. + validate_field_value(Value) -> case do_validate_field_value(Value) of true -> {ok, Value}; false -> - {error, nil} + {error, invalid_headers} end. % HTTP field values can contain: @@ -74,6 +106,31 @@ do_validate_field_value(Value) -> false end. +validate_lowercase_field_value(Value) -> + do_validate_lowercase_field_value(Value, <<>>). + +do_validate_lowercase_field_value(<<>>, Acc) -> + {ok, Acc}; +do_validate_lowercase_field_value(<<C, Rest/bits>>, Acc) when C >= $A, C =< $Z -> + do_validate_lowercase_field_value(Rest, <<Acc/binary, (C + 32)>>); +do_validate_lowercase_field_value(<<C, Rest/bits>>, Acc) + when C =:= 16#09 + orelse C >= 16#20 andalso C =< 16#7E + orelse C >= 16#80 andalso C =< 16#FF -> + do_validate_lowercase_field_value(Rest, <<Acc/binary, C>>); +do_validate_lowercase_field_value(_, _) -> + {error, invalid_headers}. + +sanitize_header_value(Value) -> + sanitize_header_value(Value, <<>>). + +sanitize_header_value(<<>>, Acc) -> + Acc; +sanitize_header_value(<<C, Rest/bitstring>>, Acc) when C =:= 16#0D; C =:= 16#0A -> + sanitize_header_value(Rest, Acc); +sanitize_header_value(<<C, Rest/bitstring>>, Acc) -> + sanitize_header_value(Rest, <<Acc/binary, C>>). + % CLOCK % -----------------------------------------------------------------------------
src/ewe/internal/encoder.gleam+15 −6 modified@@ -4,7 +4,7 @@ import gleam/int import gleam/list /// Encodes an HTTP response into bytes. -/// +/// pub fn encode_response( response: response.Response(BitArray), ) -> bytes_tree.BytesTree { @@ -15,7 +15,7 @@ pub fn encode_response( } /// Encodes the HTTP status line and headers of an HTTP response. -/// +/// pub fn encode_response_partially( response: response.Response(a), ) -> bytes_tree.BytesTree { @@ -25,7 +25,7 @@ pub fn encode_response_partially( } /// Encodes the HTTP status line. -/// +/// fn encode_status_line(status: Int) -> BitArray { let status_name = status_to_bit_array(status) let status = int.to_string(status) @@ -34,20 +34,29 @@ fn encode_status_line(status: Int) -> BitArray { } /// Encodes HTTP headers into bytes. -/// +/// fn encode_headers(headers: List(#(String, String))) -> BitArray { let headers = list.fold(headers, <<>>, fn(acc, headers) { let #(key, value) = headers - <<acc:bits, key:utf8, ": ", value:utf8, "\r\n">> + << + acc:bits, + sanitize_header_value(key):utf8, + ": ", + sanitize_header_value(value):utf8, + "\r\n", + >> }) <<headers:bits, "\r\n">> } +@external(erlang, "ewe_ffi", "sanitize_header_value") +fn sanitize_header_value(value: String) -> String + /// Maps HTTP status codes to their text descriptions. -/// +/// fn status_to_bit_array(status: Int) -> BitArray { case status { 100 -> <<"Continue">>
src/ewe/internal/http1.gleam+27 −35 modified@@ -202,8 +202,7 @@ pub fn parse_request( case connection, upgrade, settings { Ok(connection), Ok("h2c"), Ok(settings) -> { - let is_upgrade = - string.contains(string.lowercase(connection), "upgrade") + let is_upgrade = string.contains(connection, "upgrade") case is_upgrade { True -> Ok(Http2Upgrade(Upgrade(req:, settings:))) @@ -248,16 +247,14 @@ fn parse_headers( Ok(Packet(HttpHeader(idx, field, value), rest)) -> { use field <- try(case decoder.formatted_field_by_idx(idx) { Ok(field) -> Ok(field) - Error(Nil) -> { - bit_array.to_string(field) - |> result.map(string.lowercase) - |> replace_error(InvalidHeaders) - } + Error(Nil) -> validate_lowercase_field(field) }) - use value <- try( - validate_field_value(value) |> replace_error(InvalidHeaders), - ) + use value <- try(case field { + "transfer-encoding" | "connection" | "upgrade" | "expect" | "trailer" -> + validate_lowercase_field_value(value) + _ -> validate_field_value(value) + }) let new_buffer = Buffer(rest, 0) @@ -302,8 +299,14 @@ fn parse_headers( } } +@external(erlang, "ewe_ffi", "validate_lowercase_field") +fn validate_lowercase_field(field: BitArray) -> Result(String, ParseError) + @external(erlang, "ewe_ffi", "validate_field_value") -fn validate_field_value(value: BitArray) -> Result(String, Nil) +fn validate_field_value(value: BitArray) -> Result(String, ParseError) + +@external(erlang, "ewe_ffi", "validate_lowercase_field_value") +fn validate_lowercase_field_value(value: BitArray) -> Result(String, ParseError) /// Inserts a header into the headers dictionary. /// @@ -356,11 +359,7 @@ pub fn read_body( let transport = req.body.transport let socket = req.body.socket - let transfer_encoding = - request.get_header(req, "transfer-encoding") - |> result.map(string.lowercase) - - case transfer_encoding { + case request.get_header(req, "transfer-encoding") { Ok("chunked") -> { use #(body, rest_buffer) <- try(read_chunked_body( transport, @@ -379,7 +378,7 @@ pub fn read_body( trailer |> string.split(",") |> list.fold(set.new(), fn(set, field) { - set.insert(set, string.trim(field) |> string.lowercase()) + set.insert(set, string.trim(field)) }) Ok(handle_trailers(req, set, rest_buffer)) @@ -502,28 +501,25 @@ fn handle_trailers( Ok(Packet(HttpHeader(idx, field, value), header_rest)) -> { let field_name = case decoder.formatted_field_by_idx(idx) { Ok(field_name) -> Ok(field_name) - Error(Nil) -> { - bit_array.to_string(field) - |> result.map(string.lowercase) - } + Error(Nil) -> validate_lowercase_field(field) } case field_name { Ok(field_name) -> { case set.contains(set, field_name) && is_allowed_trailer(field_name) { True -> { - case bit_array.to_string(value) { + case validate_field_value(value) { Ok(value) -> { request.set_header(req, field_name, value) |> handle_trailers(set, Buffer(header_rest, 0)) } - Error(Nil) -> handle_trailers(req, set, Buffer(header_rest, 0)) + Error(_) -> handle_trailers(req, set, Buffer(header_rest, 0)) } } False -> handle_trailers(req, set, Buffer(header_rest, 0)) } } - Error(Nil) -> handle_trailers(req, set, Buffer(header_rest, 0)) + Error(_) -> handle_trailers(req, set, Buffer(header_rest, 0)) } } _ -> req @@ -759,23 +755,19 @@ pub fn upgrade_websocket( let is_upgrade = request.get_header(req, "connection") - |> result.map(fn(connection) { - string.lowercase(connection) |> string.contains("upgrade") - }) + |> result.map(string.contains(_, "upgrade")) use _ <- try(case is_upgrade { Ok(True) -> Ok(Nil) Ok(False) -> Error(InvalidConnectionHeader) Error(_) -> Error(MissingConnectionHeader) }) - use _ <- try( - case request.get_header(req, "upgrade") |> result.map(string.lowercase) { - Ok("websocket") -> Ok(Nil) - Ok(_) -> Error(InvalidUpgradeHeader) - Error(_) -> Error(MissingUpgradeHeader) - }, - ) + use _ <- try(case request.get_header(req, "upgrade") { + Ok("websocket") -> Ok(Nil) + Ok(_) -> Error(InvalidUpgradeHeader) + Error(_) -> Error(MissingUpgradeHeader) + }) use <- bool.guard( request.get_header(req, "sec-websocket-version") == Error(Nil), @@ -881,7 +873,7 @@ pub fn handle_continue(req: Request(Connection)) -> Result(Nil, ParseError) { let expect = req.headers |> list.find(fn(tupple) { - tupple.0 == "expect" && string.lowercase(tupple.1) == "100-continue" + tupple.0 == "expect" && tupple.1 == "100-continue" }) case expect {
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/vshakitskiy/ewe/commit/ce4ff214d32626a10fda9398dc94a2d720e17446nvdPatchWEB
- github.com/vshakitskiy/ewe/security/advisories/GHSA-x2w3-23jr-hrpfnvdExploitVendor AdvisoryWEB
- github.com/advisories/GHSA-x2w3-23jr-hrpfghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-34715ghsaADVISORY
- github.com/vshakitskiy/ewe/releases/tag/v3.0.6nvdProductRelease NotesWEB
News mentions
0No linked articles in our index yet.