Silent acceptance of duplicate Content-Length with divergent values
medium swesweep-fasthttp · header.go · parseHeaders (ResponseHeader) · run #4
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).
Triage
- Verdict
- INVALID confidence 0.0
- Model
- qwen/qwen3.8-27b
- Rounds
- 5
- Cost
- $0.0786 · 575.5 s
The report claims that `ResponseHeader.parseHeaders` silently ignores a second `Content-Length` header, retaining the first value. This would allow an attacker to desynchronize the parser (e.g., send `Content-Length: 10` followed by `Content-Length: 999999999`, causing the parser to expect 10 bytes while the body is larger). However, examining the provided code for `ResponseHeader.parseHeaders` (specifically the `case 'c':` block handling `strContentLength`), the logic is as follows: it parses the new value into a local variable `contentLength`. Then, it checks `if h.contentLength != -1`. If this condition is true (i.e., the connection is not currently marked as chunked), it executes `h.contentLength = contentLength`. This operation overwrites the previous value with the new one. There is no check like `if h.contentLength == -2` (initial state) to prevent overwriting a previously seen valid integer. Therefore, the behavior is 'last value wins', not 'first value wins'. Since the parser uses the last value (999999999), it expects a body of that size. While this is a protocol violation (RFC 9110 requires rejection of conflicting headers), the specific security mechanism described in the report—retention of the small header value leading to premature message boundary detection and smuggling—is factually incorrect for this code path. The bug pattern described does not exist.
CRUX: The code in ResponseHeader.parseHeaders overwrites h.contentLength with the latest value, it does not retain the first value.
GREP: if h.contentLength != -1 {All rounds
[
{
"finding_title": "Silent acceptance of duplicate Content-Length with divergent values",
"verdict": "INVALID",
"reasoning": "The report claims that in `ResponseHeader.parseHeaders`, a second `Content-Length` header with a divergent value is silently ignored, allowing an attacker to desynchronize the declared and actual body length. \n\nHowever, examining the provided code for `func (h *ResponseHeader) parseHeaders(buf []byte) (int, error)`:\n1. The variable `h.contentLength` is initialized to `-2` at the start of the function.\n2. If `Transfer-Encoding: chunked` is seen, `h.contentLength` is set to `-1`.\n3. When a `Content-Length` header is encountered:\n ```go\n if caseInsensitiveCompare(s.key, strContentLength) {\n var err error\n contentLength, err := parseContentLength(s.value)\n if err != nil {\n h.contentLength = -2\n h.connectionClose = true\n return 0, err\n }\n if h.contentLength != -1 {\n h.contentLength = contentLength\n h.contentLengthBytes = append(h.contentLengthBytes[:0], s.value...)\n }\n continue\n }\n ```\n If `h.contentLength` is currently `-2` (the default) or a value from a previous `Content-Length` header, and not `-1` (chunked), the new value overwrites the old one. \n4. Specifically, if an attacker sends `Content-Length: 10` followed by `Content-Length: 999999999`, the first sets `h.contentLength` to 10. The second iteration sees `h.contentLength` is 10 (which is `!= -1`), so it overwrites `h.contentLength` with 999999999. It does **not** silently ignore the second value; it updates to the latest value.\n\nEven if it did ignore duplicates (which it doesn't in this logic for ResponseHeaders, unlike RequestHeaders which explicitly errors on duplicates via `ErrDuplicateContentLength`), the behavior of taking the last valid value or the first valid value is a protocol handling choice. However, since the code explicitly overwrites the value, the premise that the second value is \"silently ignored\" and the first is retained is factually incorrect for the `ResponseHeader` parsing code provided. \n\nFurthermore, HTTP response smuggling is typically a server-side issue or a client-side issue where the client and server disagree. If the library is used as a client, it parses the response. If it takes the last value, that is the standard lenient behavior often seen. But the specific claim that it keeps the first and ignores the second is false based on the code `h.contentLength = contentLength`.\n\nLet's double check `RequestHeader.parseHeaders` just in case the scanner confused the two. In `RequestHeader.parseHeaders`:\n```go\nif isContentLength {\n if contentLengthSeen {\n h.connectionClose = true\n return 0, ErrDuplicateContentLength\n }\n contentLengthSeen = true\n ...\n}\n```\nSo for Requests, it errors on duplicates. For Responses, it overwrites. In neither case does it \"silently accept duplicate... with divergent values\" by keeping the first and ignoring the second while allowing the rest of the parse to proceed as if the first was the true length. The Response parser updates the length to the last seen value. This prevents the specific desync described (using 10 while body is 999999999) because the internal state would be updated to 999999999 (or rather, it would process the body based on the updated value). \n\nTherefore, the bug pattern described (silent retention of the first value) does not exist in the code. The code updates the value. Thus, the finding is a false positive.\n\nCRUX: In `ResponseHeader.parseHeaders`, does a second `Content-Length` header overwrite the first value or is it ignored?\nGREP: if h.contentLength != -1 {",
"elapsed": 109.1,
"tokens": 32690,
"tokens_in": 31798,
"tokens_out": 892,
"cost_usd": 0.01603116,
"file": "header.go",
"round": 1,
"grep_used": true,
"grep_results": "GREP `if h.contentLength != -1 { (simplified to: contentLength)`:\n```\nfasthttpadaptor/adaptor_test.go:64:\t\t\tt.Fatalf(\"unexpected contentLength %d. Expecting %d\", r.ContentLength, expectedContentLength)\nfs_test.go:162:\tcontentLength := resp.Header.ContentLength()\nfs_test.go:163:\tif contentLength != len(expectedBody) {\nfs_test.go:164:\t\tt.Fatalf(\"unexpected Content-Length: %d. expecting %d\", contentLength, len(expectedBody))\nfs_test.go:623:func testParseByteRangeSuccess(t *testing.T, v string, contentLength, startPos, endPos int) {\nfs_test.go:626:\tstartPos1, endPos1, err := ParseByteRange([]byte(v), contentLength)\nfs_test.go:628:\t\tt.Fatalf(\"unexpected error: %v. v=%q, contentLength=%d\", err, v, contentLength)\nfs_test.go:631:\t\tt.Fatalf(\"unexpected startPos=%d. Expecting %d. v=%q, contentLength=%d\", startPos1, startPos, v, contentLength)\nfs_test.go:634:\t\tt.Fatalf(\"unexpected endPos=%d. Expecting %d. v=%q, contentLength=%d\", endPos1, endPos, v, contentLength)\nfs_test.go:658:\t// byte range exceeding contentLength\nfs_test.go:665:func testParseByteRangeError(t *testing.T, v string, contentLength int) {\nfs_test.go:668:\t_, _, err := ParseByteRange([]byte(v), contentLength)\nserver_test.go:2130:\t\t\treturn headers.contentLength == acceptContentLength\nserver_test.go:2133:\t\t\tif ctx.Request.Header.contentLength != acceptContentLength {\nserver_test.go:2208:\t\t\tif ctx.Request.Header.contentLength == acceptContentLength {\nserver_test.go:2214:\t\t\tif ctx.Request.Header.contentLength != acceptContentLength {\nserver_test.go:4426:\tcontentLength := len(part1) + len(part2)\nserver_test.go:4442:\tif _, err := fmt.Fprintf(cc, \"POST /foo2 HTTP/1.1\\r\\nHost: aaa.com\\r\\nContent-Length: %d\\r\\nContent-Type: aa\\r\\n\\r\\n\", contentLength); err != nil {\nserver_test.go:4480:\tcontentLength := len(part1) + len(part2)\nserver_test.go:4497:\tif _, err := fmt.Fprintf(cc, \"POST /foo2 HTTP/1.1\\r\\nHost: aaa.com\\r\\nContent-Length: %d\\r\\nContent-Type: aa\\r\\n\\r\\n%s\", contentLength, part1); err != nil {\nserver_test.go:4528:\tcontentLength := len(content)\nserver_test.go:4533:\t\t\tif realContentLength != contentLength {\nserver_test.go:4543:\tif _, err := fmt.Fprintf(cc, \"POST /foo2 HTTP/1.1\\r\\nHost: aaa.com\\r\\nContent-Length: %d\\r\\nContent-Type: aa\\r\\n\\r\\n%s\", contentLength, content); err != nil {\nheader_test.go:1247:func testResponseHeaderSetContentRange(t *testing.T, startPos, endPos, contentLength int, expectedV string) {\nheader_test.go:1249:\th.SetContentRange(startPos, endPos, contentLength)\nheader_test.go:1252:\t\tt.Fatalf(\"unexpected content-range: %q. Expecting %q. startPos=%d, endPos=%d, contentLength=%d\",\nheader_test.go:1253:\t\t\tv, expectedV, startPos, endPos, contentLength)\nheader_test.go:1871:\tcontentLengthCount := 0\nheader_test.go:1883:\t\t\tcontentLengthCount++\nheader_test.go:1910:\tif contentLengthCount != 1 {\n```"
},
{
"finding_title": "Silent acceptance of duplicate Content-Length with divergent values",
"verdict": "INVALID",
"reasoning": "The report claims that in `ResponseHeader.parseHeaders`, a second `Content-Length` header is silently ignored if one already exists, leading to HTTP response smuggling. However, examining the provided code for `ResponseHeader.parseHeaders`: the `contentLength` field is initialized to `-2`. When a `Content-Length` header is encountered, the code checks `if h.contentLength != -1` (i.e., it is not chunked). If true, it overwrites `h.contentLength` with the newly parsed value (`h.contentLength = contentLength`). It does NOT check if `h.contentLength` is already set to a valid integer (e.g., 10) and refuse to update. Therefore, if an attacker sends `Content-Length: 10` followed by `Content-Length: 999999999`, the internal state `h.contentLength` will be updated to `999999999`. The claim that the first value is retained and the second ignored is factually incorrect based on the code logic. The library does not exhibit the specific 'silent acceptance/retention of first value' behavior described. While RFC 9110 states that a server SHOULD send an error for multiple conflicting Content-Length headers, the failure to do so is a spec violation but not necessarily a security vulnerability if the parsing logic consistently uses the last value (which prevents the specific desync where the parser uses a small length but the body is large, as the parser would now expect the large length). In fact, overwriting with the last value is generally safer than keeping the first if the body length is actually the larger one, or if the server intends to send the larger body. If the server sends a smaller body, the connection will hang, which is a liveness issue but not a smuggling attack where extra data is misinterpreted as the start of a new request (which would require the parser to consume less data than sent, leaving bytes in the buffer). Here, the parser would consume *more* data (up to 999999999), potentially consuming the next request or causing a timeout, but not smuggling. Given the specific mechanism of the reported vulnerability (retention of first value) is false, the finding is invalid.\n\nCRUX: The code in ResponseHeader.parseHeaders overwrites h.contentLength with the latest value, it does not ignore subsequent headers.\nGREP: if h.contentLength != -1 {",
"elapsed": 82.3,
"tokens": 34232,
"tokens_in": 33708,
"tokens_out": 524,
"cost_usd": 0.015729359999999998,
"file": "header.go",
"round": 2,
"grep_used": true,
"grep_results": "GREP `if h.contentLength != -1 { (simplified to: contentLength)`:\n```\nfasthttpadaptor/adaptor_test.go:64:\t\t\tt.Fatalf(\"unexpected contentLength %d. Expecting %d\", r.ContentLength, expectedContentLength)\nfs_test.go:162:\tcontentLength := resp.Header.ContentLength()\nfs_test.go:163:\tif contentLength != len(expectedBody) {\nfs_test.go:164:\t\tt.Fatalf(\"unexpected Content-Length: %d. expecting %d\", contentLength, len(expectedBody))\nfs_test.go:623:func testParseByteRangeSuccess(t *testing.T, v string, contentLength, startPos, endPos int) {\nfs_test.go:626:\tstartPos1, endPos1, err := ParseByteRange([]byte(v), contentLength)\nfs_test.go:628:\t\tt.Fatalf(\"unexpected error: %v. v=%q, contentLength=%d\", err, v, contentLength)\nfs_test.go:631:\t\tt.Fatalf(\"unexpected startPos=%d. Expecting %d. v=%q, contentLength=%d\", startPos1, startPos, v, contentLength)\nfs_test.go:634:\t\tt.Fatalf(\"unexpected endPos=%d. Expecting %d. v=%q, contentLength=%d\", endPos1, endPos, v, contentLength)\nfs_test.go:658:\t// byte range exceeding contentLength\nfs_test.go:665:func testParseByteRangeError(t *testing.T, v string, contentLength int) {\nfs_test.go:668:\t_, _, err := ParseByteRange([]byte(v), contentLength)\nserver_test.go:2130:\t\t\treturn headers.contentLength == acceptContentLength\nserver_test.go:2133:\t\t\tif ctx.Request.Header.contentLength != acceptContentLength {\nserver_test.go:2208:\t\t\tif ctx.Request.Header.contentLength == acceptContentLength {\nserver_test.go:2214:\t\t\tif ctx.Request.Header.contentLength != acceptContentLength {\nserver_test.go:4426:\tcontentLength := len(part1) + len(part2)\nserver_test.go:4442:\tif _, err := fmt.Fprintf(cc, \"POST /foo2 HTTP/1.1\\r\\nHost: aaa.com\\r\\nContent-Length: %d\\r\\nContent-Type: aa\\r\\n\\r\\n\", contentLength); err != nil {\nserver_test.go:4480:\tcontentLength := len(part1) + len(part2)\nserver_test.go:4497:\tif _, err := fmt.Fprintf(cc, \"POST /foo2 HTTP/1.1\\r\\nHost: aaa.com\\r\\nContent-Length: %d\\r\\nContent-Type: aa\\r\\n\\r\\n%s\", contentLength, part1); err != nil {\nserver_test.go:4528:\tcontentLength := len(content)\nserver_test.go:4533:\t\t\tif realContentLength != contentLength {\nserver_test.go:4543:\tif _, err := fmt.Fprintf(cc, \"POST /foo2 HTTP/1.1\\r\\nHost: aaa.com\\r\\nContent-Length: %d\\r\\nContent-Type: aa\\r\\n\\r\\n%s\", contentLength, content); err != nil {\nheader_test.go:1247:func testResponseHeaderSetContentRange(t *testing.T, startPos, endPos, contentLength int, expectedV string) {\nheader_test.go:1249:\th.SetContentRange(startPos, endPos, contentLength)\nheader_test.go:1252:\t\tt.Fatalf(\"unexpected content-range: %q. Expecting %q. startPos=%d, endPos=%d, contentLength=%d\",\nheader_test.go:1253:\t\t\tv, expectedV, startPos, endPos, contentLength)\nheader_test.go:1871:\tcontentLengthCount := 0\nheader_test.go:1883:\t\t\tcontentLengthCount++\nheader_test.go:1910:\tif contentLengthCount != 1 {\n```"
},
{
"finding_title": "Silent acceptance of duplicate Content-Length with divergent values",
"verdict": "INVALID",
"reasoning": "The reported vulnerability claims that `ResponseHeader.parseHeaders` silently ignores a second `Content-Length` header if one already exists, retaining the first value. This behavior would allow an attacker to desynchronize the parser (e.g., send `Content-Length: 10` then `Content-Length: 999999999`, causing the parser to expect 10 bytes while the body is larger, or vice versa). However, analyzing the provided code for `ResponseHeader.parseHeaders` (around line 1530 in the provided text, specifically the `case 'c':` block):\n\n```go\nif caseInsensitiveCompare(s.key, strContentLength) {\n var err error\n contentLength, err := parseContentLength(s.value)\n if err != nil { ... }\n if h.contentLength != -1 {\n h.contentLength = contentLength\n h.contentLengthBytes = append(h.contentLengthBytes[:0], s.value...)\n }\n continue\n}\n```\n\nThe code checks `if h.contentLength != -1` (meaning it is not set to chunked transfer encoding). If true, it **overwrites** `h.contentLength` with the newly parsed `contentLength`. It does **not** check if `h.contentLength` is already a valid integer and refuse to update. Therefore, if an attacker sends multiple `Content-Length` headers, the last one wins. The premise that the first value is retained and the second is ignored is factually incorrect based on the code logic. Since the specific bug pattern described (retention of the first value leading to desync) does not exist in the code, the finding is a false positive. Additionally, even if the first value were retained, the primary security risk of HTTP smuggling relies on the parser and the application disagreeing on the message boundary. Here, the parser uses the last value, which is a deterministic, if non-spec-compliant, behavior. The critical failure mode of 'silent acceptance of the first value' is absent.\n\nCRUX: In ResponseHeader.parseHeaders, does a second Content-Length header overwrite the first value or is it ignored? The code shows it overwrites.\nGREP: h.contentLength = contentLength",
"elapsed": 284.2,
"tokens": 34801,
"tokens_in": 34315,
"tokens_out": 486,
"cost_usd": 0.0158703,
"file": "header.go",
"round": 3,
"grep_used": true,
"grep_results": "GREP `= contentLength`:\n```\nserver_test.go:4533:\t\t\tif realContentLength != contentLength {\nheader.go:238:\th.contentLength = contentLength\nheader.go:269:\th.contentLength = contentLength\nheader.go:1419:\t\t\t\th.contentLength = contentLength\nheader.go:1483:\t\t\t\th.contentLength = contentLength\nheader.go:3049:\t\t\t\t\th.contentLength = contentLength\nheader.go:3231:\t\t\t\t\th.contentLength = contentLength\nheader_regression_test.go:85:\tif h.ContentLength() != contentLength {\nfs.go:1361:\tif startPos >= contentLength {\nfs.go:1373:\tif endPos >= contentLength {\nfs.go:1374:\t\tendPos = contentLength - 1\n```"
},
{
"finding_title": "Silent acceptance of duplicate Content-Length with divergent values",
"verdict": "INVALID",
"reasoning": "The report claims that `ResponseHeader.parseHeaders` silently retains the first `Content-Length` value and ignores subsequent ones, allowing an attacker to desynchronize the parser. However, examining the code for `ResponseHeader.parseHeaders`, when a `Content-Length` header is parsed, the code executes `h.contentLength = contentLength` (unless `h.contentLength` is `-1`, indicating chunked encoding). It does not check if `h.contentLength` is already set to a non-default value to prevent overwriting. Therefore, the last valid `Content-Length` header in the stream overwrites the previous ones. The specific bug pattern described in the report (retention of the first value) is factually incorrect. The code exhibits 'last value wins' behavior, not 'first value wins'. Since the described mechanism does not exist in the code, the finding is a false positive.\n\nCRUX: ResponseHeader.parseHeaders overwrites h.contentLength with the latest value, it does not retain the first value.\nGREP: if h.contentLength != -1 {\\n\\t\\t\\t\\t\\t\\t\\t\\t\\th.contentLength = contentLength",
"elapsed": 42.5,
"tokens": 34427,
"tokens_in": 34170,
"tokens_out": 257,
"cost_usd": 0.015122400000000001,
"file": "header.go",
"round": 4,
"grep_used": true,
"grep_results": "GREP `contentLength`:\n```\nfasthttpadaptor/adaptor_test.go:64:\t\t\tt.Fatalf(\"unexpected contentLength %d. Expecting %d\", r.ContentLength, expectedContentLength)\nfs_test.go:162:\tcontentLength := resp.Header.ContentLength()\nfs_test.go:163:\tif contentLength != len(expectedBody) {\nfs_test.go:164:\t\tt.Fatalf(\"unexpected Content-Length: %d. expecting %d\", contentLength, len(expectedBody))\nfs_test.go:623:func testParseByteRangeSuccess(t *testing.T, v string, contentLength, startPos, endPos int) {\nfs_test.go:626:\tstartPos1, endPos1, err := ParseByteRange([]byte(v), contentLength)\nfs_test.go:628:\t\tt.Fatalf(\"unexpected error: %v. v=%q, contentLength=%d\", err, v, contentLength)\nfs_test.go:631:\t\tt.Fatalf(\"unexpected startPos=%d. Expecting %d. v=%q, contentLength=%d\", startPos1, startPos, v, contentLength)\nfs_test.go:634:\t\tt.Fatalf(\"unexpected endPos=%d. Expecting %d. v=%q, contentLength=%d\", endPos1, endPos, v, contentLength)\nfs_test.go:658:\t// byte range exceeding contentLength\nfs_test.go:665:func testParseByteRangeError(t *testing.T, v string, contentLength int) {\nfs_test.go:668:\t_, _, err := ParseByteRange([]byte(v), contentLength)\nserver_test.go:2130:\t\t\treturn headers.contentLength == acceptContentLength\nserver_test.go:2133:\t\t\tif ctx.Request.Header.contentLength != acceptContentLength {\nserver_test.go:2208:\t\t\tif ctx.Request.Header.contentLength == acceptContentLength {\nserver_test.go:2214:\t\t\tif ctx.Request.Header.contentLength != acceptContentLength {\nserver_test.go:4426:\tcontentLength := len(part1) + len(part2)\nserver_test.go:4442:\tif _, err := fmt.Fprintf(cc, \"POST /foo2 HTTP/1.1\\r\\nHost: aaa.com\\r\\nContent-Length: %d\\r\\nContent-Type: aa\\r\\n\\r\\n\", contentLength); err != nil {\nserver_test.go:4480:\tcontentLength := len(part1) + len(part2)\nserver_test.go:4497:\tif _, err := fmt.Fprintf(cc, \"POST /foo2 HTTP/1.1\\r\\nHost: aaa.com\\r\\nContent-Length: %d\\r\\nContent-Type: aa\\r\\n\\r\\n%s\", contentLength, part1); err != nil {\nserver_test.go:4528:\tcontentLength := len(content)\nserver_test.go:4533:\t\t\tif realContentLength != contentLength {\nserver_test.go:4543:\tif _, err := fmt.Fprintf(cc, \"POST /foo2 HTTP/1.1\\r\\nHost: aaa.com\\r\\nContent-Length: %d\\r\\nContent-Type: aa\\r\\n\\r\\n%s\", contentLength, content); err != nil {\nheader_test.go:1247:func testResponseHeaderSetContentRange(t *testing.T, startPos, endPos, contentLength int, expectedV string) {\nheader_test.go:1249:\th.SetContentRange(startPos, endPos, contentLength)\nheader_test.go:1252:\t\tt.Fatalf(\"unexpected content-range: %q. Expecting %q. startPos=%d, endPos=%d, contentLength=%d\",\nheader_test.go:1253:\t\t\tv, expectedV, startPos, endPos, contentLength)\nheader_test.go:1871:\tcontentLengthCount := 0\nheader_test.go:1883:\t\t\tcontentLengthCount++\nheader_test.go:1910:\tif contentLengthCount != 1 {\n```"
},
{
"finding_title": "Silent acceptance of duplicate Content-Length with divergent values",
"verdict": "INVALID",
"reasoning": "The report claims that `ResponseHeader.parseHeaders` silently ignores a second `Content-Length` header, retaining the first value. This would allow an attacker to desynchronize the parser (e.g., send `Content-Length: 10` followed by `Content-Length: 999999999`, causing the parser to expect 10 bytes while the body is larger). However, examining the provided code for `ResponseHeader.parseHeaders` (specifically the `case 'c':` block handling `strContentLength`), the logic is as follows: it parses the new value into a local variable `contentLength`. Then, it checks `if h.contentLength != -1`. If this condition is true (i.e., the connection is not currently marked as chunked), it executes `h.contentLength = contentLength`. This operation overwrites the previous value with the new one. There is no check like `if h.contentLength == -2` (initial state) to prevent overwriting a previously seen valid integer. Therefore, the behavior is 'last value wins', not 'first value wins'. Since the parser uses the last value (999999999), it expects a body of that size. While this is a protocol violation (RFC 9110 requires rejection of conflicting headers), the specific security mechanism described in the report—retention of the small header value leading to premature message boundary detection and smuggling—is factually incorrect for this code path. The bug pattern described does not exist.\n\nCRUX: The code in ResponseHeader.parseHeaders overwrites h.contentLength with the latest value, it does not retain the first value.\nGREP: if h.contentLength != -1 {",
"elapsed": 57.4,
"tokens": 35541,
"tokens_in": 35171,
"tokens_out": 370,
"cost_usd": 0.01588182,
"file": "header.go",
"round": 5,
"grep_used": true,
"grep_results": "GREP `if h.contentLength != -1 { (simplified to: contentLength)`:\n```\nfasthttpadaptor/adaptor_test.go:64:\t\t\tt.Fatalf(\"unexpected contentLength %d. Expecting %d\", r.ContentLength, expectedContentLength)\nfs_test.go:162:\tcontentLength := resp.Header.ContentLength()\nfs_test.go:163:\tif contentLength != len(expectedBody) {\nfs_test.go:164:\t\tt.Fatalf(\"unexpected Content-Length: %d. expecting %d\", contentLength, len(expectedBody))\nfs_test.go:623:func testParseByteRangeSuccess(t *testing.T, v string, contentLength, startPos, endPos int) {\nfs_test.go:626:\tstartPos1, endPos1, err := ParseByteRange([]byte(v), contentLength)\nfs_test.go:628:\t\tt.Fatalf(\"unexpected error: %v. v=%q, contentLength=%d\", err, v, contentLength)\nfs_test.go:631:\t\tt.Fatalf(\"unexpected startPos=%d. Expecting %d. v=%q, contentLength=%d\", startPos1, startPos, v, contentLength)\nfs_test.go:634:\t\tt.Fatalf(\"unexpected endPos=%d. Expecting %d. v=%q, contentLength=%d\", endPos1, endPos, v, contentLength)\nfs_test.go:658:\t// byte range exceeding contentLength\nfs_test.go:665:func testParseByteRangeError(t *testing.T, v string, contentLength int) {\nfs_test.go:668:\t_, _, err := ParseByteRange([]byte(v), contentLength)\nserver_test.go:2130:\t\t\treturn headers.contentLength == acceptContentLength\nserver_test.go:2133:\t\t\tif ctx.Request.Header.contentLength != acceptContentLength {\nserver_test.go:2208:\t\t\tif ctx.Request.Header.contentLength == acceptContentLength {\nserver_test.go:2214:\t\t\tif ctx.Request.Header.contentLength != acceptContentLength {\nserver_test.go:4426:\tcontentLength := len(part1) + len(part2)\nserver_test.go:4442:\tif _, err := fmt.Fprintf(cc, \"POST /foo2 HTTP/1.1\\r\\nHost: aaa.com\\r\\nContent-Length: %d\\r\\nContent-Type: aa\\r\\n\\r\\n\", contentLength); err != nil {\nserver_test.go:4480:\tcontentLength := len(part1) + len(part2)\nserver_test.go:4497:\tif _, err := fmt.Fprintf(cc, \"POST /foo2 HTTP/1.1\\r\\nHost: aaa.com\\r\\nContent-Length: %d\\r\\nContent-Type: aa\\r\\n\\r\\n%s\", contentLength, part1); err != nil {\nserver_test.go:4528:\tcontentLength := len(content)\nserver_test.go:4533:\t\t\tif realContentLength != contentLength {\nserver_test.go:4543:\tif _, err := fmt.Fprintf(cc, \"POST /foo2 HTTP/1.1\\r\\nHost: aaa.com\\r\\nContent-Length: %d\\r\\nContent-Type: aa\\r\\n\\r\\n%s\", contentLength, content); err != nil {\nheader_test.go:1247:func testResponseHeaderSetContentRange(t *testing.T, startPos, endPos, contentLength int, expectedV string) {\nheader_test.go:1249:\th.SetContentRange(startPos, endPos, contentLength)\nheader_test.go:1252:\t\tt.Fatalf(\"unexpected content-range: %q. Expecting %q. startPos=%d, endPos=%d, contentLength=%d\",\nheader_test.go:1253:\t\t\tv, expectedV, startPos, endPos, contentLength)\nheader_test.go:1871:\tcontentLengthCount := 0\nheader_test.go:1883:\t\t\tcontentLengthCount++\nheader_test.go:1910:\tif contentLengthCount != 1 {\n```"
}
]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."
}
]
```