acquireRequestStream does not clamp prefetched bytes to ContentLength
high swesweep-fasthttp · streaming.go · acquireRequestStream · run #4
Description
b may contain bytes read ahead by the connection reader, including data beyond h.ContentLength() for fixed-length bodies. bytes.NewReader(b.B) exposes the entire buffer to requestStream.Read without a cap derived from the header. For a non-chunked body, the prefetched buffer should be clamped to the remaining ContentLength or the reader should enforce the ContentLength bound when reading from prefetchedBytes.
Triage
- Verdict
- INVALID confidence 0.0
- Model
- qwen/qwen3.8-27b
- Rounds
- 6
- Cost
- $0.0170 · 913.4 s
[ARBITER] The `Read` method in `requestStream` strictly enforces the `ContentLength` limit for fixed-length bodies. Specifically, it returns `io.EOF` if `totalBytesRead` equals `ContentLength` (lines 56-58). When reading from `prefetchedBytes`, it calculates the remaining limit `left` based on `ContentLength` (line 80) and truncates the read buffer `p` to this limit (lines 81-83) before calling `rs.reader.Read` (which handles the fallback if prefetched bytes are exhausted). Furthermore, when reading directly from `prefetchedBytes` (lines 60-72), it limits the read to the available prefetched size and updates `totalBytesRead`. The logic prevents reading more bytes than declared in `ContentLength` because the buffer `p` is always clamped to `ContentLength - totalBytesRead` in the path leading to the main reader, and the prefetched path also respects the total byte count. Even if `prefetchedBytes` contains more data than `ContentLength`, the `Read` method will stop returning data once `totalBytesRead` reaches `ContentLength`, preventing any exposure of data beyond the declared body size through the public API.
All rounds
[
{
"finding_title": "acquireRequestStream does not clamp prefetched bytes to ContentLength",
"verdict": "INVALID",
"reasoning": "The report claims that `acquireRequestStream` does not clamp `prefetchedBytes` to `ContentLength`, leading to a buffer over-read or logic error in `requestStream.Read`. I analyzed the `Read` method in `streaming.go`. The method first checks if `rs.header.ContentLength()` is -1 (chunked). For fixed-length bodies, it checks if `totalBytesRead == ContentLength`. If not, it checks `prefetchedBytes`. The code calculates `prefetchedSize` and limits the read from the prefetched buffer: `left := prefetchedSize - rs.totalBytesRead; if len(p) > left { p = p[:left] }`. Then it calls `rs.prefetchedBytes.Read(p)`. Crucially, `bytes.Reader.Read` will only return as many bytes as are available in its underlying buffer and also respects the limit of the slice `p` passed to it (it reads at most `len(p)` bytes). Since `p` is truncated to `left` (the number of bytes actually remaining in the prefetched buffer relative to the current read offset), `bytes.Reader` will not read beyond its internal bounds. Furthermore, even if the prefetched buffer contained more bytes than `ContentLength` allowed (which might happen if the HTTP connection reader read ahead), the `Read` function subsequently reads from `rs.reader` (the `bufio.Reader`) only up to `left := rs.header.ContentLength() - rs.totalBytesRead`. If `ContentLength` is smaller than the available data, the `Read` will stop reading from the main stream once `totalBytesRead` reaches `ContentLength`, returning `io.EOF`. The `prefetchedBytes` are just a cache of bytes already read from the stream. If the stream contains more data than `ContentLength` specifies, the excess data remains in `rs.reader` or is ignored if it was in `b.B` (which is now `prefetchedBytes`). However, `requestStream.Read` enforces `ContentLength` on every read. If the attacker sends more bytes than `ContentLength`, the `requestStream` will stop reading at `ContentLength` and return EOF. The host application (fasthttp server) will then likely see an EOF or mismatch. This does not constitute a memory safety vulnerability in `requestStream` itself. The `bytes.NewReader` is backed by the `bytebufferpool` buffer which is a valid Go allocation. Accessing it via `Read` is safe. The concern about \"exposing the entire buffer\" is mitigated by the fact that `Read` controls how many bytes are read into the destination slice `p`. There is no out-of-bounds access. The logic correctly bounds reads to `ContentLength`. Therefore, the bug is not a security vulnerability.\n\nCRUX: Does requestStream.Read enforce ContentLength limits on reads from prefetchedBytes?\nGREP: if len(p) > left",
"elapsed": 274.5,
"tokens": 3206,
"tokens_in": 2589,
"tokens_out": 617,
"cost_usd": 0.00293838,
"file": "streaming.go",
"round": 1,
"grep_used": true,
"grep_results": "GREP `if len(p) > left`:\n```\nstreaming.go:63:\t\tif len(p) > left {\n```"
},
{
"finding_title": "acquireRequestStream does not clamp prefetched bytes to ContentLength",
"verdict": "INVALID",
"reasoning": "The report claims that `acquireRequestStream` creates a `bytes.Reader` from `b.B` (prefetched bytes) without clamping it to `ContentLength`, potentially exposing data beyond the declared body size. I analyzed the `requestStream.Read` method in `streaming.go`. \n\n1. **Prefetched Bytes Handling**: When reading from `prefetchedBytes`, the code calculates `left := prefetchedSize - rs.totalBytesRead`. It then limits the read slice: `if len(p) > left { p = p[:left] }`. This ensures that at most the remaining bytes in the `prefetchedBytes` buffer are read. Since `bytes.Reader` is a standard Go type backed by a valid slice, reading within its bounds is memory-safe. The \"exposure\" mentioned is simply reading bytes that were already read from the socket into the pool buffer; `bytes.Reader.Read` does not read out-of-bounds.\n\n2. **ContentLength Enforcement**: For fixed-length bodies (`ContentLength != -1`), the code checks `if rs.totalBytesRead == rs.header.ContentLength()` at the start of `Read` and returns `io.EOF` if true. Furthermore, when reading from the main stream (`rs.reader`), it calculates `left := rs.header.ContentLength() - rs.totalBytesRead` and truncates the request buffer `p` to this size. \n\n3. **Security Impact**: Even if the `prefetchedBytes` buffer contains more data than `ContentLength` (due to read-ahead by the connection reader), the `Read` method will return those bytes until `totalBytesRead` reaches `ContentLength` (or the prefetched buffer is exhausted, at which point it switches to `rs.reader`, which is also bounded). If the handler reads exactly `ContentLength` bytes, it will receive them and then `io.EOF`. If the handler reads less, it can continue. There is no mechanism for an attacker to cause a crash, out-of-bounds access, or unauthorized data disclosure (data beyond ContentLength is either in the pool buffer which is reused or in the network stream which is part of the HTTP protocol; the library correctly enforces the length semantics defined by the Content-Length header). \n\n4. **Conclusion**: The code correctly bounds reads to the remaining `ContentLength` for the main stream and to the available bytes for the prefetched stream. The \"bug\" of not clamping the `bytes.Reader` object itself is irrelevant because `bytes.Reader` cannot read out-of-bounds, and the `Read` method logic prevents reading beyond the logical body length. This is not a security vulnerability.\n\nCRUX: Does requestStream.Read enforce ContentLength limits on reads from prefetchedBytes?\nGREP: if len(p) > left",
"elapsed": 196.6,
"tokens": 3964,
"tokens_in": 3371,
"tokens_out": 593,
"cost_usd": 0.00319482,
"file": "streaming.go",
"round": 2,
"grep_used": true,
"grep_results": "GREP `if len(p) > left`:\n```\nstreaming.go:63:\t\tif len(p) > left {\n```"
},
{
"finding_title": "acquireRequestStream does not clamp prefetched bytes to ContentLength",
"verdict": "UNCERTAIN",
"reasoning": "The report claims that `requestStream.Read` fails to clamp reads from `prefetchedBytes` to `ContentLength`, potentially allowing an attacker to read data beyond the declared HTTP body size or causing memory corruption. I analyzed the `Read` method in `streaming.go` (specifically the fixed-length body branch, `ContentLength() != -1`). \n\n1. **Enforcement Logic**: The code maintains a `totalBytesRead` counter. At the start of every `Read` call, it checks `if rs.totalBytesRead == rs.header.ContentLength()`. If true, it returns `io.EOF`. This ensures that once the logical body length is reached, no further data is served to the caller.\n2. **Prefetched Bytes Bound**: When reading from `prefetchedBytes`, the code calculates `left := prefetchedSize - rs.totalBytesRead`. It then truncates the destination buffer `p` to `left` if `len(p) > left`. This ensures the caller receives at most the remaining bytes in the prefetched buffer. Crucially, because `totalBytesRead` is incremented by every successful read, and the initial check enforces `ContentLength`, the code effectively prevents reading more than `ContentLength` bytes in total, even if the `prefetchedBytes` buffer contains ",
"elapsed": 98.1,
"tokens": 4249,
"tokens_in": 3972,
"tokens_out": 277,
"cost_usd": 0.0024992400000000002,
"file": "streaming.go",
"round": 3
},
{
"finding_title": "acquireRequestStream does not clamp prefetched bytes to ContentLength",
"verdict": "INVALID",
"reasoning": "The report claims that `acquireRequestStream` fails to clamp the `prefetchedBytes` buffer to `ContentLength`, potentially exposing data or causing security issues. I analyzed the `Read` method in `streaming.go`. For fixed-length bodies (`ContentLength != -1`), the `Read` method enforces the limit in two ways: 1) It returns `io.EOF` if `rs.totalBytesRead == rs.header.ContentLength()` at the start of any read. 2) When reading from `prefetchedBytes`, it calculates `left := prefetchedSize - rs.totalBytesRead` and truncates the destination buffer `p` to `left`. This ensures that `bytes.Reader.Read` only returns bytes available in the buffer and does not exceed the requested amount. 3) When reading from the main stream (`rs.reader`), it calculates `left := rs.header.ContentLength() - rs.totalBytesRead` and truncates `p` to this value, preventing reads beyond the declared content length. Even if the `bytebufferpool` buffer (`b`) contains more bytes than `ContentLength` (due to network read-ahead), the `Read` logic strictly bounds the number of bytes returned to the handler to `ContentLength`. Any excess data in `prefetchedBytes` or `rs.reader` is simply not returned to the handler; it remains in the buffer or stream. There is no out-of-bounds memory access because `bytes.Reader` and `bufio.Reader` are memory-safe implementations that respect the bounds of their underlying buffers. The logic correctly implements HTTP semantics for fixed-length bodies by stopping at `ContentLength`. Therefore, the alleged vulnerability does not exist, and the code is secure against the described attack.\n\nCRUX: requestStream.Read enforces ContentLength limits on all reads, preventing access to data beyond the declared body size.\nGREP: rs.totalBytesRead == rs.header.ContentLength()",
"elapsed": 53.2,
"tokens": 4639,
"tokens_in": 4231,
"tokens_out": 408,
"cost_usd": 0.00300102,
"file": "streaming.go",
"round": 4,
"grep_used": true,
"grep_results": "GREP `== rs.header.ContentLength()`:\n```\nstreaming.go:57:\tif rs.totalBytesRead == rs.header.ContentLength() {\nstreaming.go:68:\t\tif n == rs.header.ContentLength() {\nstreaming.go:83:\tif rs.totalBytesRead == rs.header.ContentLength() {\n```"
},
{
"finding_title": "acquireRequestStream does not clamp prefetched bytes to ContentLength",
"verdict": "INVALID",
"reasoning": "The vulnerability report claims that `acquireRequestStream` does not clamp `prefetchedBytes` to `ContentLength`, potentially allowing reads beyond the declared body size or causing memory safety issues. However, analyzing the `Read` method in `streaming.go` reveals that the `ContentLength` limit is strictly enforced during the read process. \n\n1. **Prefetched Bytes Enforcement**: When `ContentLength()` is not -1 (fixed-length body), the code first checks if `rs.totalBytesRead == rs.header.ContentLength()`. If true, it returns `io.EOF`. Otherwise, it calculates the remaining bytes in the prefetched buffer relative to the logical offset: `left := prefetchedSize - rs.totalBytesRead`. It then truncates the destination buffer `p` to this `left` size before calling `rs.prefetchedBytes.Read(p)`. Since `totalBytesRead` is incremented by every successful read, the effective number of bytes read from `prefetchedBytes` cannot exceed `ContentLength` (or the buffer size, if smaller). \n\n2. **Main Stream Enforcement**: If the prefetched buffer is exhausted or bypassed, the code reads from `rs.reader` (the `bufio.Reader`). Here, it calculates `left := rs.header.ContentLength() - rs.totalBytesRead` and truncates `p` accordingly. This ensures that the total number of bytes read from the network stream also does not exceed `ContentLength`. \n\n3. **Memory Safety**: `bytes.Reader` and `bufio.Reader` are memory-safe Go types that operate within the bounds of their underlying slices. There is no out-of-bounds access. Any data in `b.B` (the prefetched buffer) that exceeds `ContentLength` is simply not returned to the handler; it remains in the buffer or is left in the stream, which is consistent with HTTP semantics for fixed-length bodies (where data after the declared length is typically ignored or handled by the connection reset logic, but not exposed to the handler via `Read`). \n\nThus, the `Read` method correctly enforces the `ContentLength` limit, preventing the alleged vulnerability. The failure to clamp the `bytes.Reader` object itself in `acquireRequestStream` is not a security issue because the read logic handles the bound.\n\nCRUX: requestStream.Read enforces ContentLength limits on all reads, preventing access to data beyond the declared body size.\nGREP: rs.totalBytesRead == rs.header.ContentLength()",
"elapsed": 110.8,
"tokens": 5238,
"tokens_in": 4711,
"tokens_out": 527,
"cost_usd": 0.00355962,
"file": "streaming.go",
"round": 5,
"grep_used": true,
"grep_results": "GREP `== rs.header.ContentLength()`:\n```\nstreaming.go:57:\tif rs.totalBytesRead == rs.header.ContentLength() {\nstreaming.go:68:\t\tif n == rs.header.ContentLength() {\nstreaming.go:83:\tif rs.totalBytesRead == rs.header.ContentLength() {\n```"
},
{
"verdict": "INVALID",
"reasoning": "[ARBITER] The `Read` method in `requestStream` strictly enforces the `ContentLength` limit for fixed-length bodies. Specifically, it returns `io.EOF` if `totalBytesRead` equals `ContentLength` (lines 56-58). When reading from `prefetchedBytes`, it calculates the remaining limit `left` based on `ContentLength` (line 80) and truncates the read buffer `p` to this limit (lines 81-83) before calling `rs.reader.Read` (which handles the fallback if prefetched bytes are exhausted). Furthermore, when reading directly from `prefetchedBytes` (lines 60-72), it limits the read to the available prefetched size and updates `totalBytesRead`. The logic prevents reading more bytes than declared in `ContentLength` because the buffer `p` is always clamped to `ContentLength - totalBytesRead` in the path leading to the main reader, and the prefetched path also respects the total byte count. Even if `prefetchedBytes` contains more data than `ContentLength`, the `Read` method will stop returning data once `totalBytesRead` reaches `ContentLength`, preventing any exposure of data beyond the declared body size through the public API.",
"round": 6,
"file": "streaming.go",
"finding_title": "acquireRequestStream does not clamp prefetched bytes to ContentLength",
"elapsed": 180.2,
"tokens_in": 2268,
"tokens_out": 278,
"cost_usd": 0.00178656
}
]Raw scan report for this file
`requestStream.Read` is reachable as the body reader for parsed HTTP requests/responses. The untrusted inputs are:
- `rs.reader`: the live body/chunk stream from the network.
- `rs.prefetchedBytes`: bytes already read into `bodyBuf` before the stream is acquired.
- `rs.header.ContentLength()`: attacker-influenced `Content-Length` value.
- chunk framing bytes parsed by `parseChunkSize` and `readCrLf`.
The most important issues are in the fixed `Content-Length` path, where prefetched bytes are not clamped to the declared body length, and in the chunked path, where the parsed chunk size is used as a length/slice bound without validation.
```json
[
{
"severity": "critical",
"title": "Content-Length stream can consume bytes beyond the declared body length",
"function": "requestStream.Read",
"description": "In the fixed Content-Length branch, when rs.prefetchedBytes has remaining data, p is truncated only to prefetchedSize - totalBytesRead, not to ContentLength() - totalBytesRead. If the prefetched buffer contains more bytes than the remaining declared body length, for example read-ahead bytes or pipelined request bytes, Read can return or discard bytes that belong to the next HTTP message. After totalBytesRead exceeds ContentLength, left becomes negative and rs.reader.Read is called without truncation. This can desynchronize keep-alive connections and enable HTTP request smuggling or denial of service."
},
{
"severity": "high",
"title": "acquireRequestStream does not clamp prefetched bytes to ContentLength",
"function": "acquireRequestStream",
"description": "b may contain bytes read ahead by the connection reader, including data beyond h.ContentLength() for fixed-length bodies. bytes.NewReader(b.B) exposes the entire buffer to requestStream.Read without a cap derived from the header. For a non-chunked body, the prefetched buffer should be clamped to the remaining ContentLength or the reader should enforce the ContentLength bound when reading from prefetchedBytes."
},
{
"severity": "high",
"title": "EOF detection in prefetched Content-Length read uses per-call n instead of totalBytesRead",
"function": "requestStream.Read",
"description": "After reading from rs.prefetchedBytes, the code returns io.EOF when n == rs.header.ContentLength(). n is only the number of bytes returned by the current Read call. The correct completion test is rs.totalBytesRead == rs.header.ContentLength(). With multi-call reads or an over-sized prefetched buffer, this can signal EOF prematurely or fail to signal EOF correctly, allowing truncated bodies or incorrect connection reuse."
},
{
"severity": "medium",
"title": "Premature end-of-stream is reported as normal EOF for fixed Content-Length bodies",
"function": "requestStream.Read",
"description": "In the fixed Content-Length branch, after rs.reader.Read(p), any non-nil error is returned unchanged. If the error is io.EOF while totalBytesRead is still less than ContentLength, the stream reports normal EOF instead of io.ErrUnexpectedEOF. Callers using io.ReadAll or simple Read loops may accept a truncated body. This can be triggered by sending fewer than ContentLength bytes and closing the connection. The chunked path already converts EOF to io.ErrUnexpectedEOF, but the fixed-length path does not."
},
{
"severity": "high",
"title": "Chunk size result is not validated before being used as a slice bound",
"function": "requestStream.Read",
"description": "parseChunkSize(rs.reader) consumes attacker-controlled chunk-size lines. The returned chunkSize is stored in rs.chunkLeft without validating that it is non-negative or within policy limits. If a crafted or oversized chunk-size line causes integer overflow/wrap in the parser, chunkLeft can become negative. bytesToRead := min(rs.chunkLeft, len(p)) then yields a negative bound and p[:bytesToRead] panics with a slice out-of-range error, crashing the server. Even if non-negative, unreasonably large chunk sizes should be rejected or bounded by policy."
},
{
"severity": "low",
"title": "int64 to int truncation of prefetched buffer size",
"function": "requestStream.Read",
"description": "prefetchedSize := int(rs.prefetchedBytes.Size()) converts an int64 to int. On 32-bit targets or if the prefetched size exceeds MaxInt, the conversion can truncate or wrap, producing an incorrect or negative prefetchedSize. That can make left calculations wrong, bypass length truncation, and lead to incorrect consumption of body data or unbounded reads. Use int64 accounting or safely validate the size before conversion."
},
{
"severity": "low",
"title": "Unchecked accumulation in totalBytesRead can overflow length accounting",
"function": "requestStream.Read",
"description": "rs.totalBytesRead += n is unchecked. For very large bodies or crafted Content-Length values near the maximum int value, accumulated length can overflow and become negative. Subsequent left := ContentLength() - totalBytesRead then becomes negative, so p is not truncated before rs.reader.Read, allowing unbounded reads beyond the declared body and resource exhaustion. Use saturating arithmetic or validate accumulated length against the maximum allowed body size."
}
]
```