Unbounded trailer accumulation via direct AddTrailerBytes calls
low swesweep-fasthttp · header.go · AddTrailerBytes · run #4
Description
AddTrailerBytes is exported (via AddTrailer) and appends to h.trailer without a cap on the number of entries. A caller can invoke it repeatedly with long comma-separated lists to grow h.trailer without bound, causing memory exhaustion. While the parsing path (parseHeaders) is limited by the read buffer size, the direct public API path has no such bound.
Triage
Not triaged.
Raw scan report for this file
Looking at this file, I'll trace untrusted data flow through the HTTP header parsing and manipulation functions.
**Data flow analysis:**
- `Read(r *bufio.Reader)` → `r.Peek` → `h.parse(b)` → `parseFirstLine` / `parseHeaders` / `readRawHeaders`
- `ReadTrailer` → `parseTrailer` → mutates `h.h`
- Public setters: `AddBytesKV`, `SetBytesKV`, `SetTrailerBytes`, etc.
- `VisitHeaderParams` exposes subslices to callback
**Key observations per function:**
1. **`headerErrorMsg`** (line ~700): When `secureErrorLogMessage` is false (the zero-value default), the error message embeds `bufferSnippet(b)` — raw header bytes from the wire. An attacker sending malformed headers receives a structured error containing up to 400 bytes of raw buffer content. In a multi-tenant or proxy scenario, this leaks header data (cookies, auth tokens) into logs or client-visible responses.
2. **`tryReadTrailer`** (line ~720): `h.h = hh` is executed *before* the `errParse != nil` check. `parseTrailer` appends valid trailer entries to `dest` (aliased as `h.h`) in a loop, and only returns an error *after* the scanner loop completes (or on a per-key validation failure mid-loop). If `parseTrailer` appends 3 valid entries then hits a 4th that triggers `s.err`, the returned `hh` contains those 3 entries. `ReadTrailer` does **not** call `resetSkipNormalize()` on error (unlike `ResponseHeader.Read`), so `h.h` retains the partial trailer mutations. A caller that ignores the returned error and continues using the header will see inconsistent state.
3. **`parseHeaders` (Response)**: Duplicate `Content-Length` headers are silently accepted (first wins, second ignored without error or connection-close). An attacker sending `Content-Length: 5` followed by `Content-Length: 999999999` results in the smaller value being used while the body contains the larger amount — a potential desync between declared and actual body length.
4. **`AddTrailerBytes`**: Adds valid trailer keys to `h.trailer` incrementally. If a later key is forbidden, `err` is set but previously-added entries persist. While `SetTrailerBytes` clears `h.trailer` first, `AddTrailerBytes` can also be called directly (it's exported via `AddTrailer`). Partial trailer state persists on error.
5. **`VisitHeaderParams`**: The quoted-value parser handles `\\` escapes but does not validate that the unescaped value contains only valid header value bytes. A crafted parameter like `; x="a\rb"` passes through the escape loop (the `\r` is not a quote or backslash) and the raw `\r` byte is passed to the callback `f`. If `f` assumes RFC-7230-valid values, this violates the contract.
6. **No fixed-size buffer overflows**: All allocations are dynamic `[]byte`. The 256-byte lookup tables (`toUpperTable`, `toLowerTable`, `validHeaderValueByteTable`) are safely indexed since `byte` is 0–255. `validHeaderFieldByteTable` is 128 bytes but guarded by `c < 128`.
```json
[
{
"severity": "medium",
"title": "Information disclosure: raw header bytes in error messages",
"function": "headerErrorMsg",
"description": "When secureErrorLogMessage is false (the default zero-value), headerErrorMsg embeds bufferSnippet(b) into the returned error, exposing up to 400 bytes of raw wire data (first 200 + last 200). An attacker sending malformed headers can trigger this path and receive raw header content (cookies, Authorization tokens, custom headers) in the error string, which may be logged or propagated to clients. The buffer may also contain the beginning of the message body, further expanding the leak surface."
},
{
"severity": "medium",
"title": "Partial header state corruption after trailer parse error",
"function": "tryReadTrailer",
"description": "parseTrailer appends valid trailer key-values to dest (h.h) iteratively before the scanner error is checked. tryReadTrailer assigns h.h = hh unconditionally before checking errParse. ReadTrailer does not call resetSkipNormalize() on failure (unlike ResponseHeader.Read). If an attacker crafts a trailer section with valid entries followed by a malformed header line, h.h retains the partial trailer data. A caller that ignores the error and continues using the RequestHeader/ResponseHeader will observe corrupted header state (e.g., unexpected headers in VisitAll, incorrect Content-Length resolution)."
},
{
"severity": "medium",
"title": "Silent acceptance of duplicate Content-Length with divergent values",
"function": "parseHeaders (ResponseHeader)",
"description": "In ResponseHeader.parseHeaders, a second Content-Length header is silently ignored when h.contentLength is already set (the code checks 'if h.contentLength != -1' before overwriting, but does not flag the duplicate). An attacker can send 'Content-Length: 10' followed by 'Content-Length: 999999999'. The parser uses the first value (10), but the actual body may be 999999999 bytes. This desync between declared and actual body length can cause downstream HTTP parsers to misframe subsequent messages on a keep-alive connection (HTTP response smuggling on the client side)."
},
{
"severity": "low",
"title": "VisitHeaderParams passes unvalidated bytes through escape handling",
"function": "VisitHeaderParams",
"description": "The quoted-value branch of VisitHeaderParams scans for a closing quote while handling backslash escapes, but does not validate that the resulting value (b[m:n]) contains only valid header value bytes. A crafted parameter like '; x=\"a\\u000db\"' or containing raw control bytes will pass those bytes directly to the callback f. Callers that assume the value conforms to RFC 7230 header-value syntax (no CTLs except whitespace) may misbehave. The function's contract states it parses parameters per RFC 9110 §5.6.6, but the escape handling admits bytes outside the field-value charset."
},
{
"severity": "low",
"title": "Unbounded trailer accumulation via direct AddTrailerBytes calls",
"function": "AddTrailerBytes",
"description": "AddTrailerBytes is exported (via AddTrailer) and appends to h.trailer without a cap on the number of entries. A caller can invoke it repeatedly with long comma-separated lists to grow h.trailer without bound, causing memory exhaustion. While the parsing path (parseHeaders) is limited by the read buffer size, the direct public API path has no such bound."
}
]
```