VYPR
Medium severity5.3NVD Advisory· Published Apr 2, 2026· Updated Apr 10, 2026

CVE-2026-34715

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.

PackageAffected versionsPatched versions
eweHex
< 3.0.63.0.6

Affected products

1

Patches

1
ce4ff214d326

Merge commit from fork

https://github.com/vshakitskiy/eweVladislav ShakitskiyMar 25, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.