BudgetScan

int64 to int truncation of prefetched buffer size

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

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.

Triage

Not triaged.

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."
  }
]
```