BudgetScan

Information disclosure: raw header bytes in error messages

medium swesweep-fasthttp · header.go · headerErrorMsg · run #4

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.

Triage

Verdict
INVALID confidence 0.17
Model
qwen/qwen3.8-27b
Rounds
6
Cost
$0.0914 · 639.8 s
[ARBITER] The reported behavior is not a security vulnerability under the provided threat model. The 'leaked' data (raw header bytes) originates from the attacker's input (in the case of parsing requests) or from a trusted peer (in the case of parsing responses). The error message is returned to the application code (the library caller), which already has access to the underlying data stream or buffer. Reflecting input data back to the caller in an error message does not expose confidential information, cross-boundary data, or cause memory corruption or resource exhaustion. Therefore, it does not violate the library's security boundaries.
All rounds
[
  {
    "finding_title": "Information disclosure: raw header bytes in error messages",
    "verdict": "VALID",
    "reasoning": "The report claims that when `secureErrorLogMessage` is false (the default), the `headerErrorMsg` function embeds `bufferSnippet(b)` into the returned error, which contains up to 400 bytes of raw wire data. An attacker sending malformed headers could trigger this error path and receive raw header content in the error string. \n\n1. **Bug Pattern**: The code confirms that `headerErrorMsg` (line ~850 in the provided snippet) checks `secureErrorLogMessage`. If false, it calls `fmt.Errorf(\"... contents: %s\", ..., bufferSnippet(b))`. `bufferSnippet` returns a snippet of the input buffer `b`.\n2. **Reachability**: `headerErrorMsg` is called from `headerError`, which is called from `RequestHeader.tryRead` and `ResponseHeader.tryRead` when parsing fails (`errParse != nil`). These `Read` methods are public APIs used by the `fasthttp` server/client to read HTTP messages from the network. An attacker can send malformed HTTP headers (e.g., invalid characters, missing colon, etc.) to trigger a parse error in `parseHeaders` or `parseFirstLine`. For example, sending a header with an invalid key like `\\x00Invalid` or a malformed first line will cause `parseFirstLine` or `parseHeaders` to return an error. This error is then wrapped by `headerError` -> `headerErrorMsg`.\n3. **Impact**: The error object contains the raw bytes of the request/response. If the application logs this error (common practice) or returns it to the client (e.g., in a 400 response body via `http.ErrHandler` or custom error handling), the raw header data (which might include cookies, authorization tokens, or other sensitive headers from *other* parts of the stream if the buffer is large, or simply the malformed input itself) is exposed. More critically, `b` is the entire buffer peeked from the reader. In a server context, `b` contains the received HTTP request. If an attacker sends a malformed header *after* including sensitive headers in the same request, the entire buffer (including the sensitive headers) is included in the error message. Even if the sensitive headers are valid, the error is triggered by a *later* malformed header, but the snippet `b` covers the whole buffer (first 200 + last 200 bytes). Since headers are at the beginning, the first 200 bytes will likely contain the sensitive headers if the request is small enough. \n\nThe `secureErrorLogMessage` flag exists specifically to prevent this, but it is **false by default** (zero value). Therefore, the default behavior leaks raw wire data in error messages. This is a valid information disclosure vulnerability, especially since many web servers log errors and potentially propagate them. The \"400 bytes\" limit is not a defense against leaking sensitive headers if they are within the first 200 bytes. The attacker controls the input (the malformed header), and the leak is deterministic.\n\nCRUX: headerErrorMsg includes bufferSnippet(b) when secureErrorLogMessage is false, and secureErrorLogMessage is false by default.\nGREP: secureErrorLogMessage",
    "elapsed": 41.2,
    "tokens": 32458,
    "tokens_in": 31767,
    "tokens_out": 691,
    "cost_usd": 0.015415139999999999,
    "file": "header.go",
    "round": 1,
    "grep_used": true,
    "grep_results": "GREP `secureErrorLogMessage`:\n```\nclient.go:1557:\tresp.secureErrorLogMessage = c.SecureErrorLogMessage\nclient.go:1558:\tresp.Header.secureErrorLogMessage = c.SecureErrorLogMessage\nclient.go:1559:\treq.secureErrorLogMessage = c.SecureErrorLogMessage\nclient.go:1560:\treq.Header.secureErrorLogMessage = c.SecureErrorLogMessage\nheader_test.go:3400:\th.secureErrorLogMessage = true\nheader_test.go:3477:\th.secureErrorLogMessage = true\nheader.go:36:\tsecureErrorLogMessage bool\nheader.go:2172:\t\t\tif h.secureErrorLogMessage {\nheader.go:2187:\t\treturn headerError(\"response\", err, errParse, b, h.secureErrorLogMessage)\nheader.go:2224:\t\t\tif h.secureErrorLogMessage {\nheader.go:2243:\t\treturn headerError(\"response\", err, errParse, b, h.secureErrorLogMessage)\nheader.go:2249:func headerError(typ string, err, errParse error, b []byte, secureErrorLogMessage bool) error {\nheader.go:2251:\t\treturn headerErrorMsg(typ, errParse, b, secureErrorLogMessage)\nheader.go:2264:\t\treturn headerErrorMsg(typ, err, b, secureErrorLogMessage)\nheader.go:2267:\t\terror: headerErrorMsg(typ, ErrSmallReadBuffer, b, secureErrorLogMessage),\nheader.go:2271:func headerErrorMsg(typ string, err error, b []byte, secureErrorLogMessage bool) error {\nheader.go:2272:\tif secureErrorLogMessage {\nheader.go:2333:\t\treturn headerError(\"request\", err, errParse, b, h.secureErrorLogMessage)\nheader.go:2336:\t\treturn headerError(\"request\", err, errValidate, b, h.secureErrorLogMessage)\nheader.go:2819:\t\tif h.secureErrorLogMessage {\nheader.go:2838:\t\tif h.secureErrorLogMessage {\nheader.go:2845:\t\tif h.secureErrorLogMessage {\nheader.go:2851:\t\tif h.secureErrorLogMessage {\nheader.go:2887:\t\tif h.secureErrorLogMessage {\nheader.go:2895:\t\tif h.secureErrorLogMessage {\nheader.go:2910:\t\tif h.secureErrorLogMessage {\nheader.go:2917:\t\tif h.secureErrorLogMessage {\nheader.go:2924:\t\tif h.secureErrorLogMessage {\nheader.go:3081:\t\t\t\t\tif h.secureErrorLogMessage {\nheader.go:3089:\t\t\t\t\tif h.secureErrorLogMessage {\n```"
  },
  {
    "finding_title": "Information disclosure: raw header bytes in error messages",
    "verdict": "INVALID",
    "reasoning": "The vulnerability report claims that raw header bytes are leaked in error messages when `secureErrorLogMessage` is false (the default). Analysis confirms: 1) The bug pattern exists: `headerErrorMsg` includes `bufferSnippet(b)` (up to 400 bytes of raw wire data) when `secureErrorLogMessage` is false. 2) The code is attacker-reachable: `RequestHeader.Read` and `ResponseHeader.Read` are public APIs used to parse HTTP messages from untrusted network sources. An attacker can trigger parse errors (e.g., malformed headers) to invoke this path. 3) However, the security impact under the provided threat model is minimal. The threat model defines the attacker as 'Code that calls the public API' and the entry point as 'data it passes through'. The 'raw header data' leaked is data that the attacker themselves provided (or the host application provided). In a server context, the error message containing the raw request is returned to the application (the caller of `Read`), not directly to the remote attacker over the wire unless the application explicitly chooses to log or propagate it. Furthermore, the information leaked is the *attacker's own input* (or the client's input to the server, which is visible to the client). There is no cross-request leak, no authentication bypass, and no memory corruption. The presence of a `secureErrorLogMessage` flag indicates this is a known design choice where the default is to provide debug info (including raw data) to the application developer/monitoring, but this is not considered a security vulnerability under the model where the 'attacker' is the code calling the API or the data provider. The 'leak' is of the input to the function, not of internal system state or other users' data. Therefore, it is a code quality/configuration issue, not a security vulnerability exploitable by an external attacker to cause meaningful harm.\n\nCRUX: The error message leaks the raw input data (attacker-controlled or client-provided) back to the application caller, but does not expose internal secrets, other users' data, or allow for remote exploitation against the host process.\nGREP: headerErrorMsg",
    "elapsed": 78.7,
    "tokens": 33710,
    "tokens_in": 33246,
    "tokens_out": 464,
    "cost_usd": 0.015355319999999999,
    "file": "header.go",
    "round": 2,
    "grep_used": true,
    "grep_results": "GREP `headerErrorMsg`:\n```\nheader.go:2251:\t\treturn headerErrorMsg(typ, errParse, b, secureErrorLogMessage)\nheader.go:2264:\t\treturn headerErrorMsg(typ, err, b, secureErrorLogMessage)\nheader.go:2267:\t\terror: headerErrorMsg(typ, ErrSmallReadBuffer, b, secureErrorLogMessage),\nheader.go:2271:func headerErrorMsg(typ string, err error, b []byte, secureErrorLogMessage bool) error {\n```"
  },
  {
    "finding_title": "Information disclosure: raw header bytes in error messages",
    "verdict": "INVALID",
    "reasoning": "The report claims an information disclosure vulnerability where raw header bytes (including sensitive data like cookies/auth tokens) are exposed in error messages when `secureErrorLogMessage` is false. While the code does embed `bufferSnippet(b)` in error strings by default, this does not constitute a security vulnerability under the provided threat model for the following reasons:\n\n1. **Data Ownership**: The threat model defines the attacker as \"Code that calls the public API... and above all the data it passes through\". In the server context (the primary use case for `RequestHeader.Read`), the input buffer `b` contains the HTTP request sent by the client. The client (attacker) already possesses this data. In the client context, the input is a response from a server; leaking this back to the application developer who controls the client configuration is not a breach of an external trust boundary.\n\n2. **No External Exposure**: The error object is returned to the host application (the caller of `Read`). It is not automatically sent back to the network peer (the attacker/client) by the library. Whether this data is logged, displayed, or propagated is a decision made by the host application, not the library. The library's role is to report the parse failure; including diagnostic context (the raw bytes) is a standard debugging aid.\n\n3. **Threat Model Constraints**: The model states: \"Not a vulnerability under this model: Missing parameter validation... misuse that violates a documented precondition...\" and defines impact as \"Memory safety... correct behaviour... resource bounds\". It does not list \"leaking input data in error logs\" as a security impact. A crash, memory corruption, or unbounded resource use is required for a VALID verdict. Leaking the input itself to the caller is not a security flaw in the library; it is a configuration option (`SecureErrorLogMessage`) explicitly provided for operators to manage log hygiene.\n\n4. **Defensive Design**: The existence of the `secureErrorLogMessage` flag proves the developers are aware of the trade-off and provide a mechanism to mitigate any specific deployment concerns. The default behavior is for diagnostic utility. Marking this as a vulnerability would essentially label standard logging behavior of input data as a bug, which contradicts the principle that the library should not be held responsible for how the host application handles error logs.\n\nTherefore, this is a code quality/configuration issue, not a security vulnerability exploitable by an external attacker to cause meaningful harm (crash, RCE, auth bypass) within the library's trust boundary.\n\nCRUX: The error message returns the input data (attacker-controlled request or server-controlled response) to the application caller, which does not violate the library's security boundary as the caller already has access to the input or is the operator of the endpoint.\nGREP: SecureErrorLogMessage",
    "elapsed": 112.6,
    "tokens": 33846,
    "tokens_in": 33246,
    "tokens_out": 600,
    "cost_usd": 0.01576332,
    "file": "header.go",
    "round": 3,
    "grep_used": true,
    "grep_results": "GREP `SecureErrorLogMessage`:\n```\nclient.go:929:\tSecureErrorLogMessage bool\nclient.go:1557:\tresp.secureErrorLogMessage = c.SecureErrorLogMessage\nclient.go:1558:\tresp.Header.secureErrorLogMessage = c.SecureErrorLogMessage\nclient.go:1559:\treq.secureErrorLogMessage = c.SecureErrorLogMessage\nclient.go:1560:\treq.Header.secureErrorLogMessage = c.SecureErrorLogMessage\nserver.go:408:\tSecureErrorLogMessage bool\nserver.go:2362:\t\tctx.Request.Header.secureErrorLogMessage = s.SecureErrorLogMessage\nserver.go:2363:\t\tctx.Response.Header.secureErrorLogMessage = s.SecureErrorLogMessage\nserver.go:2364:\t\tctx.Request.secureErrorLogMessage = s.SecureErrorLogMessage\nserver.go:2365:\t\tctx.Response.secureErrorLogMessage = s.SecureErrorLogMessage\n```"
  },
  {
    "finding_title": "Information disclosure: raw header bytes in error messages",
    "verdict": "INVALID",
    "reasoning": "The vulnerability report claims that raw header bytes are leaked in error messages, potentially exposing sensitive data like cookies or authorization tokens. However, an analysis of the threat model and data flow reveals this is a false positive for the following reasons: 1. **Data Ownership**: In the server context (where untrusted input enters), the buffer `b` contains the HTTP request sent by the client (the attacker). The error message is returned to the application's internal code (the host of the fasthttp library). The attacker already possesses the data being 'leaked' in their own request. 2. **No External Exposure**: The error object is not automatically sent back to the remote attacker. It is returned to the local application logic. Whether the application logs or propagates this data is a decision made by the host application, not the library. The library's role is to report parse failures, and including diagnostic context (the raw bytes) is standard behavior for debugging. 3. **Threat Model Scope**: The threat model defines security impacts as memory safety issues, crashes, or unbounded resource usage. Leaking the input data itself back to the caller does not constitute a security vulnerability within the library's trust boundary, as the caller already has access to the input or is the operator of the endpoint. The existence of the `SecureErrorLogMessage` flag provides a mechanism for operators to manage log hygiene, confirming this is a configuration/design choice rather than a flaw in the library's security model.\n\nCRUX: The error message returns the input data (attacker-controlled request) to the application caller, which does not violate the library's security boundary as the caller already has access to the input.\nGREP: secureErrorLogMessage",
    "elapsed": 77.6,
    "tokens": 34402,
    "tokens_in": 34037,
    "tokens_out": 365,
    "cost_usd": 0.01539054,
    "file": "header.go",
    "round": 4,
    "grep_used": true,
    "grep_results": "GREP `secureErrorLogMessage`:\n```\nclient.go:1557:\tresp.secureErrorLogMessage = c.SecureErrorLogMessage\nclient.go:1558:\tresp.Header.secureErrorLogMessage = c.SecureErrorLogMessage\nclient.go:1559:\treq.secureErrorLogMessage = c.SecureErrorLogMessage\nclient.go:1560:\treq.Header.secureErrorLogMessage = c.SecureErrorLogMessage\nheader_test.go:3400:\th.secureErrorLogMessage = true\nheader_test.go:3477:\th.secureErrorLogMessage = true\nheader.go:36:\tsecureErrorLogMessage bool\nheader.go:2172:\t\t\tif h.secureErrorLogMessage {\nheader.go:2187:\t\treturn headerError(\"response\", err, errParse, b, h.secureErrorLogMessage)\nheader.go:2224:\t\t\tif h.secureErrorLogMessage {\nheader.go:2243:\t\treturn headerError(\"response\", err, errParse, b, h.secureErrorLogMessage)\nheader.go:2249:func headerError(typ string, err, errParse error, b []byte, secureErrorLogMessage bool) error {\nheader.go:2251:\t\treturn headerErrorMsg(typ, errParse, b, secureErrorLogMessage)\nheader.go:2264:\t\treturn headerErrorMsg(typ, err, b, secureErrorLogMessage)\nheader.go:2267:\t\terror: headerErrorMsg(typ, ErrSmallReadBuffer, b, secureErrorLogMessage),\nheader.go:2271:func headerErrorMsg(typ string, err error, b []byte, secureErrorLogMessage bool) error {\nheader.go:2272:\tif secureErrorLogMessage {\nheader.go:2333:\t\treturn headerError(\"request\", err, errParse, b, h.secureErrorLogMessage)\nheader.go:2336:\t\treturn headerError(\"request\", err, errValidate, b, h.secureErrorLogMessage)\nheader.go:2819:\t\tif h.secureErrorLogMessage {\nheader.go:2838:\t\tif h.secureErrorLogMessage {\nheader.go:2845:\t\tif h.secureErrorLogMessage {\nheader.go:2851:\t\tif h.secureErrorLogMessage {\nheader.go:2887:\t\tif h.secureErrorLogMessage {\nheader.go:2895:\t\tif h.secureErrorLogMessage {\nheader.go:2910:\t\tif h.secureErrorLogMessage {\nheader.go:2917:\t\tif h.secureErrorLogMessage {\nheader.go:2924:\t\tif h.secureErrorLogMessage {\nheader.go:3081:\t\t\t\t\tif h.secureErrorLogMessage {\nheader.go:3089:\t\t\t\t\tif h.secureErrorLogMessage {\n```"
  },
  {
    "finding_title": "Information disclosure: raw header bytes in error messages",
    "verdict": "UNCERTAIN",
    "reasoning": "The report claims that raw header bytes are leaked in error messages when `secureErrorLogMessage` is false, allowing an attacker to expose sensitive data (like cookies or Authorization tokens) via logs or client responses. \n\nAnalysis: \n1. **Bug Existence**: The code in `headerErrorMsg` confirms that when `secureErrorLogMessage` is false (the default), it includes `bufferSnippet(b)` in the error message. `b` is the raw buffer read from the network. \n2. **Data Ownership & Reachability**: The input `b` comes from the network connection. \n   - In a **Server** context, `b` is the HTTP request sent by the client (the attacker). The attacker already knows the content of the request they sent. Leaking their own input back to the server's application (via the error object) does not expose new secret information to the attacker. The ",
    "elapsed": 277.4,
    "tokens": 35104,
    "tokens_in": 34904,
    "tokens_out": 200,
    "cost_usd": 0.01525968,
    "file": "header.go",
    "round": 5
  },
  {
    "verdict": "INVALID",
    "reasoning": "[ARBITER] The reported behavior is not a security vulnerability under the provided threat model. The 'leaked' data (raw header bytes) originates from the attacker's input (in the case of parsing requests) or from a trusted peer (in the case of parsing responses). The error message is returned to the application code (the library caller), which already has access to the underlying data stream or buffer. Reflecting input data back to the caller in an error message does not expose confidential information, cross-boundary data, or cause memory corruption or resource exhaustion. Therefore, it does not violate the library's security boundaries.",
    "round": 6,
    "file": "header.go",
    "finding_title": "Information disclosure: raw header bytes in error messages",
    "elapsed": 52.3,
    "tokens_in": 32871,
    "tokens_out": 139,
    "cost_usd": 0.01422282
  }
]
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."
  }
]
```