BudgetScan

Premature end-of-stream is reported as normal EOF for fixed Content-Length bodies

medium swesweep-fasthttp · streaming.go · requestStream.Read · run #4

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.

Triage

Verdict
INVALID confidence 0.0
Model
qwen/qwen3.8-27b
Rounds
5
Cost
$0.0151 · 505.7 s
The reported bug is a semantic correctness issue in error handling, not a security vulnerability under the provided threat model. 

1. **Bug Verification**: The code in `streaming.go` for the fixed `Content-Length` branch indeed returns any error from `rs.reader.Read(p)` unchanged. If the connection is closed prematurely, `bufio.Reader.Read` returns `io.EOF`. Since `totalBytesRead < ContentLength`, the stream is truncated, but the error returned is `io.EOF` instead of `io.ErrUnexpectedEOF`. The chunked branch explicitly converts `io.EOF` to `io.ErrUnexpectedEOF` (line 50), highlighting the inconsistency. The bug pattern exists.

2. **Security Impact**: Under the 'library' threat model, security vulnerabilities are defined by memory safety violations (OOB, UAF), crashes, or unbounded resource consumption. This bug does not cause any of these.
   - **Memory Safety**: The library correctly bounds reads to `len(p)` and `ContentLength`. It does not write out of bounds or read past available buffer space. 
   - **Crash/Exploit**: Returning `io.EOF` is a valid `io.Reader` behavior for an exhausted stream. It does not crash the process. It may mislead a caller into thinking the body is complete, but this is a data integrity issue at the application logic level, not a compromise of the library's memory integrity. 
   - **Reachability**: While an attacker can trigger this by sending fewer bytes than `Content-Length` and closing the connection, the consequence is incorrect error signaling, not a security breach. 

3. **Threat Model Compliance**: The threat model explicitly states that 'code quality issues not security vulnerabilities' include missing NULL checks on internal APIs or undefined behavior in debug builds. More broadly, it defines valid vulnerabilities as those causing 'meaningful harm (crash, code execution, data corruption, auth bypass)'. A truncated body being misidentified as complete is a functional defect (reliability/correctness) but does not constitute a security vulnerability in the library itself (no memory corruption, no auth bypass via the library, no crash). Therefore, it is INVALID.

CRUX: Whether returning io.EOF instead of io.ErrUnexpectedEOF on a truncated fixed-length body constitutes a security vulnerability (memory corruption/crash) or merely a code quality/correctness issue under the library threat model.
GREP: ErrUnexpectedEOF
All rounds
[
  {
    "finding_title": "Premature end-of-stream is reported as normal EOF for fixed Content-Length bodies",
    "verdict": "INVALID",
    "reasoning": "The reported vulnerability claims that a truncated HTTP body (where the connection closes before `Content-Length` bytes are received) results in `io.EOF` instead of `io.ErrUnexpectedEOF` in the fixed `Content-Length` branch of `requestStream.Read`. I analyzed the code in `streaming.go`: in the fixed length path, if `rs.reader.Read(p)` returns an error (such as `io.EOF` due to connection closure), the code executes `if err != nil { return n, err }`. It does not convert `io.EOF` to `io.ErrUnexpectedEOF` if `totalBytesRead < ContentLength`. This is a behavioral deviation from standard `io.Reader` semantics for fixed-size streams, where `io.ErrUnexpectedEOF` is expected to signal truncation. However, under the provided threat model, this is a code quality/correctness issue, not a security vulnerability. The library correctly prevents reading past the declared length (no buffer overflow) and correctly stops reading when the connection is closed. The failure to distinguish between 'normal end of stream' and 'unexpected end of stream' in this specific implementation detail does not lead to memory corruption, code execution, or data corruption. Callers using `io.ReadAll` might treat a truncated body as a complete one, but this is an application-level handling of malformed/truncated input, not a crash or exploit in the library itself. The library's primary security boundaries (memory safety) are intact.\n\nCRUX: Whether returning io.EOF instead of io.ErrUnexpectedEOF on a truncated fixed-length body constitutes a security vulnerability (memory corruption/crash) or merely a code quality/correctness issue under the library threat model.\nGREP: ErrUnexpectedEOF",
    "elapsed": 88.8,
    "tokens": 2976,
    "tokens_in": 2613,
    "tokens_out": 363,
    "cost_usd": 0.00218646,
    "file": "streaming.go",
    "round": 1,
    "grep_used": true,
    "grep_results": "GREP `ErrUnexpectedEOF`:\n```\nfasthttpadaptor/adaptor_test.go:340:\t\tif err != nil && err != io.ErrUnexpectedEOF {\nfasthttpadaptor/adaptor_test.go:426:\t\tif err != nil && err != io.ErrUnexpectedEOF {\nstreaming.go:50:\t\t\terr = io.ErrUnexpectedEOF\nserver.go:174:\t//   * io.ErrUnexpectedEOF\nhttp.go:1459:\t\t\t\treturn ErrBrokenChunk{error: io.ErrUnexpectedEOF}\nhttp.go:1600:\t\t\t\treturn ErrBrokenChunk{error: io.ErrUnexpectedEOF}\nhttp.go:2696:\t\t\t\treturn dst[:offset], io.ErrUnexpectedEOF\n```"
  },
  {
    "finding_title": "Premature end-of-stream is reported as normal EOF for fixed Content-Length bodies",
    "verdict": "INVALID",
    "reasoning": "The reported vulnerability describes a discrepancy in error handling: for fixed Content-Length bodies, if the connection closes prematurely (returning io.EOF from the underlying reader) before all declared bytes are read, `requestStream.Read` returns `io.EOF` instead of `io.ErrUnexpectedEOF`. \n\n1. **Bug Existence**: The code confirms this. In the fixed-length branch (lines 68-78), if `rs.reader.Read(p)` returns an error, it is returned immediately (`return n, err`). Since `bufio.Reader.Read` returns `io.EOF` when the underlying stream ends, a truncated body results in `io.EOF` being propagated to the caller, provided `totalBytesRead` has not yet reached `ContentLength`. The chunked branch explicitly converts `io.EOF` to `io.ErrUnexpectedEOF` (lines 49-51), highlighting the inconsistency.\n\n2. **Security Impact**: Under the provided threat model for a **library**, the assets are \"Memory safety and integrity... correct behaviour on malformed input\". The key question is whether this error masking causes meaningful harm (crash, RCE, data corruption). \n   - It does **not** cause memory corruption: the library correctly bounds reads by `ContentLength` and `len(p)`. It does not write out-of-bounds. \n   - It does **not** cause a crash in the library itself: returning `io.EOF` is a valid `io.Reader` contract behavior for an exhausted stream, even if semantically misleading for a truncated body. \n   - The impact is purely on the **consumer** (the host application). If a handler uses `io.ReadAll` or similar, it might assume the body is complete when it is truncated. This is a data integrity issue for the application's logic, but the library is functioning as an I/O abstraction. \n\n3. **Threat Model Application**: The threat model states: \"A crash, memory corruption or unbounded resource use reachable through the public API with a crafted payload is VALID.\" It also notes that \"correct behaviour on malformed input\" is an asset. While distinguishing EOF from UnexpectedEOF is a correctness issue, it is generally classified as a **bug** or **quality issue** rather than a **security vulnerability** unless it leads to bypassing authentication, executing code, or crashing the process. Standard `io` packages in Go often leave it to the specific implementation to decide, but `io.ErrUnexpectedEOF` is the *conventional* signal. However, in the context of a security scan, this is a **false positive** for a *security* vulnerability because it does not compromise the host process's memory safety or integrity directly. It is a functional defect. Most security frameworks would classify this as a reliability or data integrity issue, not a CVE-worthy security hole unless specifically used for auth bypass (which is not the case here, as the error is still returned, just with a different code). \n\nTherefore, it is not a security vulnerability under the strict definition of memory safety/crash/exploit.\n\nCRUX: Whether returning io.EOF instead of io.ErrUnexpectedEOF on a truncated body leads to memory corruption, crash, or significant security compromise, or if it is merely a semantic correctness issue.\nGREP: ErrUnexpectedEOF",
    "elapsed": 141.6,
    "tokens": 3976,
    "tokens_in": 3270,
    "tokens_out": 706,
    "cost_usd": 0.0034914,
    "file": "streaming.go",
    "round": 2,
    "grep_used": true,
    "grep_results": "GREP `ErrUnexpectedEOF`:\n```\nfasthttpadaptor/adaptor_test.go:340:\t\tif err != nil && err != io.ErrUnexpectedEOF {\nfasthttpadaptor/adaptor_test.go:426:\t\tif err != nil && err != io.ErrUnexpectedEOF {\nstreaming.go:50:\t\t\terr = io.ErrUnexpectedEOF\nserver.go:174:\t//   * io.ErrUnexpectedEOF\nhttp.go:1459:\t\t\t\treturn ErrBrokenChunk{error: io.ErrUnexpectedEOF}\nhttp.go:1600:\t\t\t\treturn ErrBrokenChunk{error: io.ErrUnexpectedEOF}\nhttp.go:2696:\t\t\t\treturn dst[:offset], io.ErrUnexpectedEOF\n```"
  },
  {
    "finding_title": "Premature end-of-stream is reported as normal EOF for fixed Content-Length bodies",
    "verdict": "INVALID",
    "reasoning": "The reported bug is that `requestStream.Read` in the fixed `Content-Length` branch returns `io.EOF` when the underlying stream closes before `Content-Length` bytes are read, rather than `io.ErrUnexpectedEOF`. Code inspection confirms this behavior: in `streaming.go`, the fixed-length path executes `if err != nil { return n, err }` immediately after `rs.reader.Read(p)`, propagating `io.EOF` from `bufio.Reader` without checking if `rs.totalBytesRead < rs.header.ContentLength()`. The chunked path, in contrast, explicitly converts `io.EOF` to `io.ErrUnexpectedEOF` (line 50). While this is a deviation from standard `io.Reader` semantics for fixed-size streams, the threat model for this library explicitly defines security vulnerabilities as issues affecting 'Memory safety and integrity of the host process (out-of-bounds access, use-after-free, type confusion)' or causing 'crash... [or] unbounded resource use'. This bug results in incorrect error signaling (a functional correctness issue) but does not cause memory corruption, out-of-bounds writes, or crashes within the library. The library correctly bounds reads to the declared length and does not write past buffer limits. The impact is that a host application might fail to detect a truncated body, which is a data integrity issue at the application level, not a security vulnerability in the library itself under the provided threat model.\n\nCRUX: Whether returning io.EOF instead of io.ErrUnexpectedEOF on a truncated body constitutes a security vulnerability (memory safety/crash) or merely a code quality/correctness issue under the library threat model.\nGREP: ErrUnexpectedEOF",
    "elapsed": 95.8,
    "tokens": 4390,
    "tokens_in": 4033,
    "tokens_out": 357,
    "cost_usd": 0.00276486,
    "file": "streaming.go",
    "round": 3,
    "grep_used": true,
    "grep_results": "GREP `ErrUnexpectedEOF`:\n```\nfasthttpadaptor/adaptor_test.go:340:\t\tif err != nil && err != io.ErrUnexpectedEOF {\nfasthttpadaptor/adaptor_test.go:426:\t\tif err != nil && err != io.ErrUnexpectedEOF {\nstreaming.go:50:\t\t\terr = io.ErrUnexpectedEOF\nserver.go:174:\t//   * io.ErrUnexpectedEOF\nhttp.go:1459:\t\t\t\treturn ErrBrokenChunk{error: io.ErrUnexpectedEOF}\nhttp.go:1600:\t\t\t\treturn ErrBrokenChunk{error: io.ErrUnexpectedEOF}\nhttp.go:2696:\t\t\t\treturn dst[:offset], io.ErrUnexpectedEOF\n```"
  },
  {
    "finding_title": "Premature end-of-stream is reported as normal EOF for fixed Content-Length bodies",
    "verdict": "INVALID",
    "reasoning": "The reported bug is technically accurate in its description of the code path: in the fixed `Content-Length` branch of `requestStream.Read`, if `rs.reader.Read(p)` returns an error (specifically `io.EOF` due to connection closure) before `totalBytesRead` reaches `ContentLength`, the error is returned unchanged. It is not converted to `io.ErrUnexpectedEOF`. The chunked branch does explicitly convert `io.EOF` to `io.ErrUnexpectedEOF`. However, under the provided threat model for a library, security vulnerabilities are defined by impacts such as memory safety violations (out-of-bounds, use-after-free), crashes, or unbounded resource consumption. This issue is a semantic correctness bug in error reporting. It allows a caller to potentially misinterpret a truncated body as a complete one if they strictly check for `io.EOF` rather than `io.ErrUnexpectedEOF` or rely on `io.ReadAll`'s default behavior which may mask the truncation error in some contexts (though `io.ReadAll` typically propagates the specific error). However, the library itself remains memory-safe; it does not read out-of-bounds, it does not corrupt memory, and it does not crash the host process. The impact is confined to the application logic potentially processing incomplete data, which is a functional defect or code quality issue, not a security vulnerability that leads to exploitation of the library's integrity. Since the threat model explicitly excludes issues that do not result in memory safety breaches, crashes, or resource exhaustion from being classified as security vulnerabilities, this finding is INVALID.\n\nCRUX: The bug causes incorrect error signaling (io.EOF instead of io.ErrUnexpectedEOF) but does not lead to memory corruption, crashes, or unbounded resource usage within the library, thus falling under code quality rather than security vulnerability in this threat model.\nGREP: ErrUnexpectedEOF",
    "elapsed": 141.1,
    "tokens": 4860,
    "tokens_in": 4469,
    "tokens_out": 391,
    "cost_usd": 0.00304998,
    "file": "streaming.go",
    "round": 4,
    "grep_used": true,
    "grep_results": "GREP `ErrUnexpectedEOF`:\n```\nfasthttpadaptor/adaptor_test.go:340:\t\tif err != nil && err != io.ErrUnexpectedEOF {\nfasthttpadaptor/adaptor_test.go:426:\t\tif err != nil && err != io.ErrUnexpectedEOF {\nstreaming.go:50:\t\t\terr = io.ErrUnexpectedEOF\nserver.go:174:\t//   * io.ErrUnexpectedEOF\nhttp.go:1459:\t\t\t\treturn ErrBrokenChunk{error: io.ErrUnexpectedEOF}\nhttp.go:1600:\t\t\t\treturn ErrBrokenChunk{error: io.ErrUnexpectedEOF}\nhttp.go:2696:\t\t\t\treturn dst[:offset], io.ErrUnexpectedEOF\n```"
  },
  {
    "finding_title": "Premature end-of-stream is reported as normal EOF for fixed Content-Length bodies",
    "verdict": "INVALID",
    "reasoning": "The reported bug is a semantic correctness issue in error handling, not a security vulnerability under the provided threat model. \n\n1. **Bug Verification**: The code in `streaming.go` for the fixed `Content-Length` branch indeed returns any error from `rs.reader.Read(p)` unchanged. If the connection is closed prematurely, `bufio.Reader.Read` returns `io.EOF`. Since `totalBytesRead < ContentLength`, the stream is truncated, but the error returned is `io.EOF` instead of `io.ErrUnexpectedEOF`. The chunked branch explicitly converts `io.EOF` to `io.ErrUnexpectedEOF` (line 50), highlighting the inconsistency. The bug pattern exists.\n\n2. **Security Impact**: Under the 'library' threat model, security vulnerabilities are defined by memory safety violations (OOB, UAF), crashes, or unbounded resource consumption. This bug does not cause any of these.\n   - **Memory Safety**: The library correctly bounds reads to `len(p)` and `ContentLength`. It does not write out of bounds or read past available buffer space. \n   - **Crash/Exploit**: Returning `io.EOF` is a valid `io.Reader` behavior for an exhausted stream. It does not crash the process. It may mislead a caller into thinking the body is complete, but this is a data integrity issue at the application logic level, not a compromise of the library's memory integrity. \n   - **Reachability**: While an attacker can trigger this by sending fewer bytes than `Content-Length` and closing the connection, the consequence is incorrect error signaling, not a security breach. \n\n3. **Threat Model Compliance**: The threat model explicitly states that 'code quality issues not security vulnerabilities' include missing NULL checks on internal APIs or undefined behavior in debug builds. More broadly, it defines valid vulnerabilities as those causing 'meaningful harm (crash, code execution, data corruption, auth bypass)'. A truncated body being misidentified as complete is a functional defect (reliability/correctness) but does not constitute a security vulnerability in the library itself (no memory corruption, no auth bypass via the library, no crash). Therefore, it is INVALID.\n\nCRUX: Whether returning io.EOF instead of io.ErrUnexpectedEOF on a truncated fixed-length body constitutes a security vulnerability (memory corruption/crash) or merely a code quality/correctness issue under the library threat model.\nGREP: ErrUnexpectedEOF",
    "elapsed": 38.4,
    "tokens": 5461,
    "tokens_in": 4939,
    "tokens_out": 522,
    "cost_usd": 0.0036403800000000004,
    "file": "streaming.go",
    "round": 5,
    "grep_used": true,
    "grep_results": "GREP `ErrUnexpectedEOF`:\n```\nfasthttpadaptor/adaptor_test.go:340:\t\tif err != nil && err != io.ErrUnexpectedEOF {\nfasthttpadaptor/adaptor_test.go:426:\t\tif err != nil && err != io.ErrUnexpectedEOF {\nstreaming.go:50:\t\t\terr = io.ErrUnexpectedEOF\nserver.go:174:\t//   * io.ErrUnexpectedEOF\nhttp.go:1459:\t\t\t\treturn ErrBrokenChunk{error: io.ErrUnexpectedEOF}\nhttp.go:1600:\t\t\t\treturn ErrBrokenChunk{error: io.ErrUnexpectedEOF}\nhttp.go:2696:\t\t\t\treturn dst[:offset], io.ErrUnexpectedEOF\n```"
  }
]
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."
  }
]
```