BudgetScan

readBodyIdentity allows unbounded body growth when maxBodySize <= 0

medium swesweep-fasthttp · http.go · readBodyIdentity · run #4

Description

Called with maxBodySize=0 (the default from Request.Read / ReadLimitBody(r, 0)) for requests without Content-Length or Transfer-Encoding, the function reads from the network until EOF with no size cap. The buffer doubles each time it fills (roundUpForSliceCap(2 * offset)). An attacker who controls connection lifetime (e.g. a slow drip over a long-lived TCP connection) forces the server to grow the body buffer without bound until OOM. No per-request size limit is enforced.

Triage

Verdict
INVALID confidence 0.0
Model
qwen/qwen3.8-27b
Rounds
6
Cost
$0.0837 · 840.6 s
[ARBITER] The vulnerability report claims that `readBodyIdentity` is reached for requests without Content-Length or Transfer-Encoding (identity body) when `maxBodySize` is 0, allowing unbounded growth. However, tracing the code path for `Request.Read` (which defaults `maxBodySize` to 0) shows that `ContinueReadBody` explicitly checks if `contentLength == -2` (which indicates an identity body, i.e., no Content-Length or Transfer-Encoding). In this case, the function returns `nil` immediately without calling `ReadBody` or `readBodyIdentity`. Therefore, `readBodyIdentity` is not called for standard requests with unknown length via the public `Request.Read` API. While `readBodyIdentity` itself would allow unbounded growth if called with `maxBodySize <= 0` and a stream that doesn't close, the specific attack vector described in the report (exploiting the default `Request.Read` path for identity bodies) is blocked by the `contentLength == -2` check. For responses, `Response.Read` can call `readBodyIdentity` if the response has no Content-Length and is not chunked, but this typically requires the connection to close to terminate the stream, and more importantly, the report specifically cites the `Request` path which is invalid. Even if reachable in Response, the threat model usually focuses on parsing untrusted input (requests) or the server enforcing limits; however, the primary claim about the Request path is factually incorrect due to the guard.
All rounds
[
  {
    "finding_title": "readBodyIdentity allows unbounded body growth when maxBodySize <= 0",
    "verdict": "UNCERTAIN",
    "reasoning": "{\"reasoning\":\"The vulnerability report claims that `readBodyIdentity` allows unbounded body growth when `maxBodySize <= 0`. \\n\\n1. **Bug Pattern Verification**: The code for `readBodyIdentity` is:\\n```go\\nfunc readBodyIdentity(r *bufio.Reader, maxBodySize int, dst []byte) ([]byte, error) {\\n    ...\\n    for {\\n        nn, err := r.Read(dst[offset:])\\n        ...\\n        offset += nn\\n        if maxBodySize > 0 && offset > maxBodySize {\\n            return dst[:offset], ErrBodyTooLarge\\n        }\\n        if len(dst) == offset {\\n            n := roundUpForSliceCap(2 * offset)\\n            if maxBodySize > 0 && n > maxBodySize {\\n                n = maxBodySize + 1\\n            }\\n            b := make([]byte, n)\\n            copy(b, dst)\\n            dst = b\\n        }\\n    }\\n}\\n```\\nIf `maxBodySize` is 0 (or negative), the check `maxBodySize > 0` fails, so the size limit is not enforced. The buffer doubles in size (`roundUpForSliceCap(2 * offset)`) indefinitely until `io.EOF` is reached or an error occurs. This confirms the bug pattern exists.\\n\\n2. **Attacker Reachability**: The function `readBodyIdentity` is called from `req.ReadBody` and `resp.ReadBody` when `contentLength` is `-2` (identity body, no Content-Length or Transfer-Encoding header). \\n   - For requests: `req.ReadLimitBody` is called with `maxBodySize` typically 0 by default (from `req.Read(r)` which calls `req.ReadLimitBody(r, 0)`). \\n   - The code path: `req.ReadBody(r, contentLength, maxBodySize)` -> if `contentLength == -2` -> `readBodyIdentity(r, maxBodySize, bodyBuf.B)`. \\n   - If an attacker sends a request without `Content-Length` or `Transfer-Encoding` (which is allowed for identity bodies in some contexts, though rare for requests as per RFC 7230, fasthttp may accept it or handle it specifically), and keeps the connection open, dripping data slowly, the server will allocate memory without bound. \\n   - More critically, this is a **library**. The threat model states: \\\"Attacker: Code that calls the public API... parsers, decoders... receive adversarial payloads\\\". The `Request.Read` / `ReadLimitBody` is a public API. If a server developer uses `req.Read(r)` (default limit 0) on a connection where the client can control the stream lifetime and content (e.g., a malicious client), and the request has no Content-Length, the server will read until EOF. \\n   - However, does the HTTP protocol allow a request without Content-Length or Transfer-Encoding? RFC 7230 says: \\\"A user agent SHOULD NOT send a Transfer-Encoding header field in any request message with a body.\\\" and \\\"If a body message-length is not defined by a Content-Length header field... it is defined by the end of the message body...\\\". For requests, this usually implies connection close. If the server keeps the connection alive (keep-alive) and the client does not close it, and sends no Content-Length, the server will read until EOF (connection close). If the client *does* close the connection, the buffer stops growing. But if the client sends data slowly over a long-lived connection *without* closing, and without Content-Length, the server buffers it. \\n   - Is this a security vulnerability? It's an OOM via slow drip attack. The library provides `ReadLimitBody(r, limit)` to protect against this. The default `Read(r)` passes 0. Is it the library's responsibility to enforce a default limit? \\n   - The report states \\\"Called with maxBodySize=0 (the default from Request.Read / ReadLimitBody(r, 0))\\\". \\n   - Let's check if there is a hardcoded default in the library that prevents this. Grep results show `defaultMaxInMemoryFileSize` but that's for multipart. `maxSmallFileSize` is 1048576. \\n   - The library explicitly provides `ReadLimitBody` to allow callers to set a limit. If the caller uses `Read`, they accept the default behavior. \\n   - Is this a bug in the *library* or a misconfiguration in the *app*? The threat model says: \\\"Not a vulnerability under this model: misuse that violates a documented precondition of the public API.\\\" Does the doc say you must use `ReadLimitBody`? `Request.Read` docs: \\\"Read reads request (including body) from the given r... io.EOF is returned if r is closed before reading the first header byte.\\\" It doesn't explicitly warn against unbounded memory. \\n   - However, many libraries (like Go's `http.ReadRequest`) have a default limit or rely on the connection close. In fasthttp, `Request.Read` with no Content-Length leads to `readBodyIdentity` which reads until EOF. If the attacker controls the connection lifetime, they can cause OOM. \\n   - Is this \\\"meaningful harm\\\"? OOM crash is meaningful. \\n   - Is it \\\"attacker-reachable\\\"? Yes, via a crafted HTTP request without Content-Length. \\n   - Is there a defense? The caller *can* use `ReadLimitBody`. But the default `Read` is vulnerable. \\n   - Compare with `readBodyChunked` which also takes `maxBodySize`. If 0, it also grows unbounded? `readBodyChunked` checks `if maxBodySize > 0 && len(dst)+chunkSize > maxBodySize`. So yes, both are vulnerable to the same issue if limit is 0. \\n   - Is the lack of a default limit a security flaw? In many contexts, reading until EOF for identity bodies is standard (e.g., `curl`). But for a server, it's risky. \\n   - Let's look at the threat model again: \\\"A crash, memory corruption or unbounded resource use reachable through the public API with a crafted payload is VALID.\\\" \\n   - The public API `Request.Read` allows unbounded resource use if the payload is crafted (no Content-Length, slow drip). \\n   - Therefore, this is a valid finding under the strict threat model of a library where unbounded resource use via public API is a vulnerability.\\n\\nWait, let's double check if `Read` actually goes to `readBodyIdentity` for requests. \\n`ContinueReadBody`: \\n```go\\nif contentLength == -2 {\\n    // identity body has no sense for http requests, since\\n    // the end of body is determined by connection close.\\n    // So just ignore request body for requests without\\n    // 'Content-Length' and 'Transfer-Encoding' headers.\\n    // refer to https://tools.ietf.org/html/rfc7230#section-3.3.2\\n    if !req.Header.ignoreBody() {\\n        req.Header.SetContentLength(0)\\n    }\\n    return nil\\n}\\n```\\nAh! For **requests**, if `contentLength == -2` (no Content-Length, no Transfer-Encoding), the code **ignores the body** and sets Content-Length to 0. It does **NOT** call `ReadBody` for the identity case in requests. \\n\\nLet's verify `readLimitBody` -> `ContinueReadBody`. \\nYes, `ContinueReadBody` handles `contentLength == -2` by returning `nil` immediately (after resetting content length). \\n\\nSo, for **requests**, the unbounded `readBodyIdentity` path is **never reached** via the standard `Read`/`ReadLimitBody` path because identity bodies are ignored for requests. \\n\\nWhat about **responses**? \\n`Response.ReadBody`: \\n```go\\ndefault:\\n    if resp.StreamBody {\\n        resp.bodyStream = acquireRequestStream(bodyBuf, r, &resp.Header)\\n    } else {\\n        bodyBuf.B, err = readBodyIdentity(r, maxBodySize, bodyBuf.B)\\n        resp.Header.SetContentLength(len(bodyBuf.B))\\n    }\\n```\\nFor responses, `contentLength` can be `-2` if there is no Content-Length and no Transfer-Encoding. This is valid for HTTP/1.0 responses or some streaming scenarios. If a client library (like `Client`) reads a response from a server, and the server sends a response without Content-Length and without Transfer-Encoding, and keeps the connection open, the client will read until EOF. If the server is malicious (or the man-in-the-middle), they can drip data slowly. \\n\\nIs this a vulnerability in `fasthttp`? \\nIf `fasthttp` is used as a **client** (e.g., `client.Get`), and the server is malicious, the client can be OOMed. \\nThe threat model includes: \\\"Attacker: Code that calls the public API... and above all the data it passes through...\\\" and \\\"Entry points... Public... functions... and every buffer, string, file or stream handed to them.\\\" \\nA malicious server interacting with a fasthttp client is an attacker. \\nThe default limit is 0. \\nThus, a malicious server can OOM a fasthttp client. \\n\\nHowever, usually clients have timeouts. If the client has a timeout, the read will eventually fail. But memory allocation happens *during* the read. If the drip is slow but steady, memory grows. \\n\\nIs this considered a valid vulnerability in the library? \\nThe library provides `ReadLimitBody` to allow setting a limit. The fact that the default is 0 (unlimited) is a design choice. \\nHowever, the prompt says: \\\"VALID: ... unbounded resource use reachable through the public API with a crafted payload is VALID.\\\" \\n\\nLet's check if there is any implicit limit. \\nIn `readBodyIdentity`, there is no other limit. \\n\\nIs it common to mark this as INVALID because it's \\\"expected behavior\\\" for identity encoding? \\nIn Go's standard `net/http`, `Request.Body` is read until EOF. It doesn't have a built-in size limit for identity bodies unless you wrap it. \\nBut `fasthttp` is a high-performance library often used in resource-constrained environments. \\n\\nLet's look at the specific report: \\\"readBodyIdentity allows unbounded body growth when maxBodySize <= 0\\\". \\nIf this is only reachable via `Response.Read` (client side) or internal paths, is it a vulnerability? \\nFor a library, allowing a remote server to OOM the client is a DoS vulnerability. \\n\\nHowever, look at the code for `Response.ReadLimitBody`. It is public. \\nIf I write `resp.Read(r)` where `r` is a network conn, I accept the default. \\n\\nLet's consider the \"}",
    "elapsed": 166.3,
    "tokens": 26974,
    "tokens_in": 24585,
    "tokens_out": 2389,
    "cost_usd": 0.0174927,
    "file": "http.go",
    "round": 1
  },
  {
    "finding_title": "readBodyIdentity allows unbounded body growth when maxBodySize <= 0",
    "verdict": "UNCERTAIN",
    "reasoning": "The report claims that `readBodyIdentity` allows unbounded body growth when called with `maxBodySize <= 0` (the default). To verify this, I traced the execution path for both `Request` and `Response` objects.\n\n1. **Request Path**: The entry point is `Request.ReadLimitBody` (called with `maxBodySize=0` by `Request.Read`). This calls `readLimitBody`, which in turn calls `ContinueReadBody`. Inside `ContinueReadBody`, the code checks `contentLength`.\n   - If `contentLength >= 0`, it reads with a limit (if `maxBodySize > 0`) or fixed size.\n   - If `contentLength == -1` (chunked), it calls `readBodyChunked`.\n   - If `contentLength == -2` (identity, i.e., no Content-Length and no Transfer-Encoding), the code explicitly handles this case:\n     ```go\n     if contentLength == -2 {\n         // identity body has no sense for http requests, since\n         // the end of body is determined by connection close.\n         // So just ignore request body for requests without\n         // 'Content-Length' and 'Transfer-Encoding' headers.\n         // refer to https://tools.ietf.org/html/rfc7230#section-3.3.2\n         if !req.Header.ignoreBody() {\n             req.Header.SetContentLength(0)\n         }\n         return nil\n     }\n     ```\n     It returns `nil` immediately without reading any body. It does NOT call `readBodyIdentity`. Therefore, for requests, the unbounded identity path is never reached via the public API when the body size is unknown (-2). The only way to reach `readBodyIdentity` in a request would be if `contentLength` was somehow negative but not -1 or -2, which is not a valid state for `ContentLength()` in fasthttp (it returns -1 for chunked, -2 for identity, or >= 0 for fixed). Even if it did, the default `maxBodySize=0` would allow unbounded reading, but the path is guarded.\n\n2. **Response Path**: The entry point is `Response.ReadLimitBody` (called with `maxBodySize=0` by `Response.Read`). This calls `Response.ReadBody`. Inside `ReadBody`, it checks `contentLength`.\n   - If `contentLength == -2` (identity), and `resp.StreamBody` is false, it calls `readBodyIdentity(r, maxBodySize, bodyBuf.B)`.\n   - `readBodyIdentity` reads until EOF or error. If `maxBodySize` is 0, it doubles the buffer indefinitely.\n   - This path IS reachable. A malicious server can send a response without `Content-Length` or `Transfer-Encoding` (which is valid for HTTP/1.0 or certain streaming scenarios) and keep the connection open, dripping data slowly. A fasthttp client using `Response.Read` (or `Client.Do` which uses default limits) would allocate unbounded memory.\n\nHowever, the threat model for a library states: \"Attacker: Code that calls the public API... parsers, decoders, deserializers and protocol handlers receive adversarial payloads that the host application obtained from untrusted sources.\" It also lists \"unbounded resource use reachable through the public API with a crafted payload is VALID\".\n\nWhile the response path is technically reachable, is it a *vulnerability* in the library's design? The library provides `ReadLimitBody` to allow setting limits. The default `Read` passes 0. In many HTTP libraries, identity encoding relies on connection close. If the connection is kept open by the attacker, the client reads until close. This is standard behavior for identity encoding. The ",
    "elapsed": 120.8,
    "tokens": 27933,
    "tokens_in": 27111,
    "tokens_out": 822,
    "cost_usd": 0.013852619999999998,
    "file": "http.go",
    "round": 2
  },
  {
    "finding_title": "readBodyIdentity allows unbounded body growth when maxBodySize <= 0",
    "verdict": "INVALID",
    "reasoning": "The report claims that `readBodyIdentity` allows unbounded body growth when `maxBodySize <= 0`, specifically citing the default path from `Request.Read` / `ReadLimitBody(r, 0)` for requests without Content-Length or Transfer-Encoding. \n\n1. **Request Path Analysis**: Tracing the code for `Request.Read`:\n   - `Request.Read` calls `Request.ReadLimitBody(r, 0)`.\n   - `ReadLimitBody` calls `req.readLimitBody`.\n   - `readLimitBody` calls `req.ContinueReadBody`.\n   - In `ContinueReadBody`, the code checks `contentLength`.\n   - If `contentLength == -2` (which indicates an identity body, i.e., no Content-Length and no Transfer-Encoding), the code executes:\n     ```go\n     if contentLength == -2 {\n         // identity body has no sense for http requests, since\n         // the end of body is determined by connection close.\n         // So just ignore request body for requests without\n         // 'Content-Length' and 'Transfer-Encoding' headers.\n         // refer to https://tools.ietf.org/html/rfc7230#section-3.3.2\n         if !req.Header.ignoreBody() {\n             req.Header.SetContentLength(0)\n         }\n         return nil\n     }\n     ```\n   - It returns `nil` immediately **without** calling `req.ReadBody`. Consequently, `readBodyIdentity` is **never reached** for requests via the standard public API path when the body size is unknown (-2). The library explicitly guards against reading the body in this scenario for requests, treating it as having no body. Thus, an attacker cannot trigger unbounded reading of a request body via the default `Request.Read` API by omitting headers.\n\n2. **Response Path Analysis**: \n   - For `Response.Read`, if `contentLength == -2` and `StreamBody` is false, it does call `readBodyIdentity(r, maxBodySize, bodyBuf.B)`. \n   - If `maxBodySize` is 0 (default), this reads until EOF. If a malicious server sends a response without Content-Length/Transfer-Encoding and keeps the connection open, dripping data, the client will buffer it in memory indefinitely until EOF or a timeout. \n   - However, the vulnerability report specifically cites \"Called with maxBodySize=0 (the default from Request.Read / ReadLimitBody(r, 0)) for requests...\". The premise regarding the Request path is factually incorrect due to the guard in `ContinueReadBody`. \n   - While the Response path exists, in the context of a client library, reading a response body until EOF is the standard semantic for identity encoding. The library provides `ReadLimitBody` for callers who wish to impose a limit. The default behavior of reading until EOF for identity responses is consistent with standard HTTP semantics (RFC 7230). More importantly, the specific attack vector described in the report (attacker controls connection lifetime to force server OOM via Request.Read) is invalid because the server does not read the body in that case. The report conflates the Request and Response paths or fails to notice the Request guard. Given the specific claim about `Request.Read` being vulnerable, and that this is the primary threat vector for server-side DoS, the finding is invalid for the stated reason.\n\n3. **Conclusion**: The specific bug pattern described (unbounded growth in `readBodyIdentity` triggered by `Request.Read` for identity bodies) does not exist because `Request.ContinueReadBody` explicitly skips body reading when `contentLength == -2` for requests. The data flow does not reach `readBodyIdentity` for requests in this state.\n\nCRUX: Request.ContinueReadBody returns nil immediately when contentLength == -2, preventing readBodyIdentity from being called for requests with unknown length.\nGREP: contentLength == -2",
    "elapsed": 222.0,
    "tokens": 28775,
    "tokens_in": 27920,
    "tokens_out": 855,
    "cost_usd": 0.0142914,
    "file": "http.go",
    "round": 3,
    "grep_used": true,
    "grep_results": "GREP `contentLength == -2`:\n```\nhttp.go:1439:\tif contentLength == -2 {\nhttp.go:1521:\tif contentLength == -2 {\nheader.go:3118:\tif h.contentLength == -2 && !h.ConnectionUpgrade() && !h.mustSkipContentLength() {\n```"
  },
  {
    "finding_title": "readBodyIdentity allows unbounded body growth when maxBodySize <= 0",
    "verdict": "INVALID",
    "reasoning": "The vulnerability report claims that `readBodyIdentity` allows unbounded body growth when `maxBodySize <= 0`, specifically citing the default path from `Request.Read` for requests without `Content-Length` or `Transfer-Encoding`. I traced the code path for `Request.Read`: 1. `Request.Read` calls `Request.ReadLimitBody(r, 0)`. 2. `ReadLimitBody` calls `req.readLimitBody`, which calls `req.ContinueReadBody`. 3. In `ContinueReadBody`, the code checks `contentLength`. For a request without `Content-Length` or `Transfer-Encoding`, `contentLength` is -2. 4. The code explicitly handles this case: `if contentLength == -2 { ... return nil }`. It sets `ContentLength` to 0 (for non-ignorable bodies) and returns immediately without reading the body. 5. Consequently, `req.ReadBody` is never called for identity bodies in requests, and `readBodyIdentity` is never reached via the public `Request.Read` API for this scenario.  The report's premise regarding the Request path is factually incorrect. While the Response path (`Response.Read` -> `Response.ReadBody` -> `readBodyIdentity`) does exist and can read until EOF, the specific attack vector described in the report (server-side DoS via `Request.Read` handling unbounded identity bodies) is invalid because the library explicitly guards against it. The threat model for a library includes unbounded resource use, but the specific bug reported (unbounded request body reading) does not exist in the code due to the guard in `ContinueReadBody`. The Response path is standard HTTP semantic for identity encoding (read until connection close/EOF) and the library provides `ReadLimitBody` for callers who wish to impose limits, but the reported finding specifically targets the Request path which is protected.\n\nCRUX: Request.ContinueReadBody returns nil immediately when contentLength == -2, preventing readBodyIdentity from being called for requests with unknown length.\nGREP: contentLength == -2",
    "elapsed": 94.5,
    "tokens": 29280,
    "tokens_in": 28836,
    "tokens_out": 444,
    "cost_usd": 0.01344312,
    "file": "http.go",
    "round": 4,
    "grep_used": true,
    "grep_results": "GREP `contentLength == -2`:\n```\nheader.go:3118:\tif h.contentLength == -2 && !h.ConnectionUpgrade() && !h.mustSkipContentLength() {\nhttp.go:1439:\tif contentLength == -2 {\nhttp.go:1521:\tif contentLength == -2 {\n```"
  },
  {
    "finding_title": "readBodyIdentity allows unbounded body growth when maxBodySize <= 0",
    "verdict": "INVALID",
    "reasoning": "The report claims that `readBodyIdentity` allows unbounded body growth when called via `Request.Read` for requests without Content-Length or Transfer-Encoding. I traced the code path for `Request.Read`: it calls `ReadLimitBody(r, 0)`, which calls `readLimitBody`, which calls `ContinueReadBody`. Inside `ContinueReadBody`, if the `contentLength` is -2 (which indicates an identity body, i.e., no Content-Length and no Transfer-Encoding header), the code explicitly handles this case by setting the content length to 0 (if the body is not ignored) and returning `nil` immediately. It does not proceed to call `req.ReadBody` or `readBodyIdentity`. Therefore, for HTTP requests, the unbounded read path via `readBodyIdentity` is not reachable when the body size is unknown (identity encoding). The library treats requests without Content-Length as having no body, which aligns with RFC 7230 for requests where the body length is not explicitly defined by a header (though strictly, identity encoding relies on connection close, fasthttp's choice to not buffer it is a security feature, not a vulnerability, in this context). While `Response.Read` can reach `readBodyIdentity` for identity responses, the specific attack vector described in the report (attacker triggering server OOM via `Request.Read`) is invalid because the code path is guarded. The report's premise that `Request.Read` leads to unbounded reading for identity bodies is factually incorrect for this library's implementation.\n\nCRUX: Request.ContinueReadBody returns nil immediately when contentLength == -2, preventing readBodyIdentity from being called for requests with unknown length.\nGREP: contentLength == -2",
    "elapsed": 139.2,
    "tokens": 29718,
    "tokens_in": 29347,
    "tokens_out": 371,
    "cost_usd": 0.01343874,
    "file": "http.go",
    "round": 5,
    "grep_used": true,
    "grep_results": "GREP `contentLength == -2`:\n```\nheader.go:3118:\tif h.contentLength == -2 && !h.ConnectionUpgrade() && !h.mustSkipContentLength() {\nhttp.go:1439:\tif contentLength == -2 {\nhttp.go:1521:\tif contentLength == -2 {\n```"
  },
  {
    "verdict": "INVALID",
    "reasoning": "[ARBITER] The vulnerability report claims that `readBodyIdentity` is reached for requests without Content-Length or Transfer-Encoding (identity body) when `maxBodySize` is 0, allowing unbounded growth. However, tracing the code path for `Request.Read` (which defaults `maxBodySize` to 0) shows that `ContinueReadBody` explicitly checks if `contentLength == -2` (which indicates an identity body, i.e., no Content-Length or Transfer-Encoding). In this case, the function returns `nil` immediately without calling `ReadBody` or `readBodyIdentity`. Therefore, `readBodyIdentity` is not called for standard requests with unknown length via the public `Request.Read` API. While `readBodyIdentity` itself would allow unbounded growth if called with `maxBodySize <= 0` and a stream that doesn't close, the specific attack vector described in the report (exploiting the default `Request.Read` path for identity bodies) is blocked by the `contentLength == -2` check. For responses, `Response.Read` can call `readBodyIdentity` if the response has no Content-Length and is not chunked, but this typically requires the connection to close to terminate the stream, and more importantly, the report specifically cites the `Request` path which is invalid. Even if reachable in Response, the threat model usually focuses on parsing untrusted input (requests) or the server enforcing limits; however, the primary claim about the Request path is factually incorrect due to the guard.",
    "round": 6,
    "file": "http.go",
    "finding_title": "readBodyIdentity allows unbounded body growth when maxBodySize <= 0",
    "elapsed": 97.8,
    "tokens_in": 24231,
    "tokens_out": 328,
    "cost_usd": 0.01116102
  }
]
Raw scan report for this file

# Security Analysis: `http.go` (fasthttp)

## Step-by-Step Trace of Untrusted Data Flows

### 1. `readBodyChunked` → `parseChunkSize` → `appendBodyFixedSize` (CRITICAL)

**Flow:** Network bytes → `Transfer-Encoding: chunked` body → `readBodyChunked(r, maxBodySize, dst)` → `parseChunkSize(r)` returns `chunkSize` → `appendBodyFixedSize(r, dst, chunkSize+strCRLFLen)`

**Vulnerability:** `parseChunkSize` calls `readHexInt(r)` to parse the chunk size. If an attacker sends a chunk size hex value that exceeds `MaxInt` (e.g., `ffffffffffffffff`), the integer wraps to a **negative** value. The subsequent check:

```go
if maxBodySize > 0 && len(dst)+chunkSize > maxBodySize {
    return dst, ErrBodyTooLarge
}
```

passes because `len(dst) + negative_value` is less than `maxBodySize`. Then:

```go
dst, err = appendBodyFixedSize(r, dst, chunkSize+strCRLFLen)
```

With `chunkSize = -3` (from hex `fffffffffffffffd`), `chunkSize + strCRLFLen = -1`. Inside `appendBodyFixedSize`:

```go
dstLen := offset + n  // 0 + (-1) = -1
dst = dst[:dstLen]    // PANIC: slice bounds out of range
```

**Impact:** Remote DoS — server process crashes. No authentication required.

---

### 2. `appendBodyFixedSize` accepts negative `n` (CRITICAL)

The function only guards against `n == 0`, not `n < 0`:

```go
func appendBodyFixedSize(r *bufio.Reader, dst []byte, n int) ([]byte, error) {
    if n == 0 {
        return dst, nil
    }
    offset := len(dst)
    dstLen := offset + n
    if cap(dst) < dstLen {  // false when dstLen < 0
        ...
    }
    dst = dst[:dstLen]  // PANIC if dstLen < 0
    ...
}
```

Any negative `n` causes `dst[:dstLen]` to panic. This is the concrete crash primitive triggered by the chunk-size overflow above.

---

### 3. Decompression Bomb via Default No-Limit Methods (HIGH)

```go
func (req *Request) BodyGunzip() ([]byte, error) {
    return req.BodyGunzipWithLimit(0)  // 0 = no limit
}
```

All of `BodyGunzip`, `BodyInflate`, `BodyUnbrotli`, `BodyUnzstd`, and `BodyUncompressed` default to `maxBodySize = 0` (unbounded). An attacker sends a ~25-byte gzip body that decompresses to multi-GB. The library allocates the full decompressed size into `bb.B` with no cap → OOM / process kill.

The `WithLimit` variants exist, but the convenience methods that most code uses first are the dangerous ones.

---

### 4. Multipart Form + Gzip with No Size Limit (HIGH)

In `MultipartFormWithLimit(0)` (called by `MultipartForm()`), the streaming + gzip path:

```go
if bytes.Equal(ce, strGzip) {
    if bodyStream, err = gzip.NewReader(bodyStream); err != nil { ... }
}
if maxBodySize > 0 {
    lr = &io.LimitedReader{ R: bodyStream, N: int64(maxBodySize) + 1 }
    bodyStream = lr
}
// maxBodySize == 0 → no LimitedReader
mr := multipart.NewReader(bodyStream, req.multipartFormBoundary)
req.multipartForm, err = mr.ReadForm(8 * 1024)
```

When `maxBodySize <= 0`, the gzip decompressed stream is read **without any size cap**. A crafted `Content-Encoding: gzip` + `Content-Type: multipart/form-data` request with a small compressed payload causes unbounded memory growth in `mr.ReadForm`.

---

### 5. `readBodyIdentity` — Unbounded Allocation when `maxBodySize <= 0` (MEDIUM)

```go
func readBodyIdentity(r *bufio.Reader, maxBodySize int, dst []byte) ([]byte, error) {
    ...
    for {
        nn, err := r.Read(dst[offset:])
        ...
        offset += nn
        if maxBodySize > 0 && offset > maxBodySize {
            return dst[:offset], ErrBodyTooLarge
        }
        if len(dst) == offset {
            n := roundUpForSliceCap(2 * offset)
            // no cap when maxBodySize <= 0
            b := make([]byte, n)
            ...
        }
    }
}
```

When called with `maxBodySize <= 0` (the default via `Request.Read(r)` → `ReadLimitBody(r, 0)`), an attacker who controls connection lifetime (no `Content-Length`, no `Transfer-Encoding`) can send data indefinitely, forcing the server to grow the buffer without bound until OOM.

---

### 6. `ContinueReadBodyStream` — `ErrBodyTooLarge` Leaves Oversized Stream Active (MEDIUM)

```go
if err == ErrBodyTooLarge {
    req.Header.SetContentLength(contentLength)  // original, oversized value
    req.body = bodyBuf
    req.bodyStream = acquireRequestStream(bodyBuf, r, &req.Header)
    return nil  // no error to caller!
}
```

When the pre-read exceeds `maxBodySize`, the function **silently succeeds** and hands back a stream that will continue reading the full (oversized) body. The caller receives `nil` error and a `Content-Length` that exceeds the configured limit. If downstream code trusts `Content-Length` for buffer sizing, this can lead to unexpected large allocations.

---

## JSON Findings

```json
[
  {
    "severity": "critical",
    "title": "Integer overflow in chunked Transfer-Encoding chunk size causes panic (remote DoS)",
    "function": "readBodyChunked",
    "description": "parseChunkSize() calls readHexInt() which can overflow Go's signed int when the attacker supplies a 16-hex-digit chunk size >= 0x8000000000000000. The resulting negative chunkSize bypasses the 'len(dst)+chunkSize > maxBodySize' check (negative + small <= max), then appendBodyFixedSize() is called with chunkSize+strCRLFLen. When the sum is negative (e.g. hex 'fffffffffffffffd' → -3 + 2 = -1), appendBodyFixedSize executes 'dst = dst[:dstLen]' with a negative dstLen, panicking with 'slice bounds out of range' and crashing the server process. Triggerable by any client sending a chunked body with a crafted chunk size; no authentication required."
  },
  {
    "severity": "critical",
    "title": "appendBodyFixedSize lacks negative-size guard, panics on negative n",
    "function": "appendBodyFixedSize",
    "description": "The function only returns early when n == 0 but does not validate n < 0. A negative n produces a negative dstLen (offset + n), and the subsequent 'dst = dst[:dstLen]' panics. This is the crash primitive reached via the chunk-size integer overflow in readBodyChunked, and could also be triggered by any other code path that passes an unvalidated negative size (e.g. an overflowed Content-Length if header parsing is bypassed)."
  },
  {
    "severity": "high",
    "title": "Decompression bomb via default no-limit BodyGunzip/BodyInflate/BodyUnbrotli/BodyUnzstd/BodyUncompressed",
    "function": "BodyGunzip",
    "description": "BodyGunzip(), BodyInflate(), BodyUnbrotli(), BodyUnzstd(), and BodyUncompressed() all call their WithLimit counterparts with maxBodySize=0, meaning no cap on decompressed output. An attacker sends a minimal gzip/deflate/brotli/zstd payload (~100 bytes) that decompresses to several GB. gunzipData/inflateData/etc. allocate the full decompressed size into a ByteBuffer with no bound, causing OOM and process termination. The convenience (no-limit) methods are the most commonly called entry points, making this the default behavior for any application that does not explicitly use the WithLimit variants."
  },
  {
    "severity": "high",
    "title": "Unbounded gzip decompression in MultipartFormWithLimit(0) streaming path",
    "function": "MultipartFormWithLimit",
    "description": "When a multipart/form-data request has Content-Encoding: gzip and arrives as a body stream, MultipartFormWithLimit(0) (the default from MultipartForm()) wraps the stream in gzip.NewReader but only applies an io.LimitedReader when maxBodySize > 0. With the default limit of 0, the decompressed stream is passed to multipart.NewReader().ReadForm() without any size cap. A small gzipped body containing a high-ratio gzip bomb causes unbounded memory allocation during multipart parsing, leading to OOM/DoS."
  },
  {
    "severity": "medium",
    "title": "readBodyIdentity allows unbounded body growth when maxBodySize <= 0",
    "function": "readBodyIdentity",
    "description": "Called with maxBodySize=0 (the default from Request.Read / ReadLimitBody(r, 0)) for requests without Content-Length or Transfer-Encoding, the function reads from the network until EOF with no size cap. The buffer doubles each time it fills (roundUpForSliceCap(2 * offset)). An attacker who controls connection lifetime (e.g. a slow drip over a long-lived TCP connection) forces the server to grow the body buffer without bound until OOM. No per-request size limit is enforced."
  },
  {
    "severity": "medium",
    "title": "ContinueReadBodyStream silently returns oversized body stream on ErrBodyTooLarge",
    "function": "ContinueReadBodyStream",
    "description": "When readBodyWithStreaming detects that contentLength exceeds maxBodySize, the function returns nil (no error) and sets req.bodyStream to a requestStream that will continue reading the full oversized body from the network. The Content-Length header is set to the original (oversized) value. Downstream code that uses Content-Length to pre-allocate buffers or that assumes the body is within the configured limit will process an unbounded amount of data, defeating the purpose of the size limit."
  }
]
```