BudgetScan

Unbounded gzip decompression in MultipartFormWithLimit(0) streaming path

high swesweep-fasthttp · http.go · MultipartFormWithLimit · run #4

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.

Triage

Verdict
VALID confidence 0.67
Model
qwen/qwen3.8-27b
Rounds
6
Cost
$0.0777 · 808.2 s
[ARBITER] The vulnerability exists in the `MultipartFormWithLimit` function within `http.go`. When processing a request with a `bodyStream` (common for large uploads or when using `ReadLimitBody` with streaming) and `Content-Encoding: gzip`, the code wraps the stream in `gzip.NewReader`. If `maxBodySize` is 0 (which is the default when calling `MultipartForm()`, which delegates to `MultipartFormWithLimit(0)`), no `io.LimitedReader` is applied to the decompressed stream. This results in `multipart.NewReader` receiving an unbounded reader. An attacker can send a small gzip-compressed body with a high compression ratio (gzip bomb) that decompresses to a massive size. The `ReadForm` method will then allocate unbounded memory to store the form fields or files, leading to an Out-Of-Memory (OOM) condition and Denial of Service. The library does not enforce a default limit on the decompressed size when the limit is explicitly set to 0 or not provided via the default `MultipartForm()` call.
All rounds
[
  {
    "finding_title": "Unbounded gzip decompression in MultipartFormWithLimit(0) streaming path",
    "verdict": "INVALID",
    "reasoning": "The report claims that `MultipartFormWithLimit(0)` allows unbounded memory allocation when processing a gzip-compressed multipart body stream. I examined the `MultipartFormWithLimit` function in `http.go`. In the streaming path (`req.bodyStream != nil`), if `Content-Encoding` is `gzip`, the code wraps the body stream in a `gzip.NewReader`. If `maxBodySize > 0`, it wraps this in an `io.LimitedReader`. However, if `maxBodySize` is 0 (the default), no `LimitedReader` is applied. The decompressed stream is then passed to `multipart.NewReader(...).ReadForm(8 * 1024)`. The `ReadForm` method of `multipart.Reader` will read file parts into memory up to the specified size (8KB) but will write larger parts to temporary files. Crucially, the memory allocated for the decompression buffer and the multipart parsing logic (headers, small parts) is bounded by the structure of the multipart data and the temporary file threshold. A gzip bomb typically involves a small compressed size decompressing to a huge size. However, `multipart/form-data` requires specific boundaries. An attacker cannot create a valid multipart stream where the entire decompressed payload is a single huge binary blob without headers, because `ReadForm` parses the structure. More importantly, even if a huge file part is created, it is written to a temp file, not memory. The memory consumption is bounded by the multipart headers and the 8KB in-memory limit for parts. The `gzip.NewReader` itself does not allocate unbounded memory for the decompressed output; it streams it. The `multipart.Reader` streams it to disk or memory (bounded). Therefore, there is no unbounded *memory* allocation that leads to OOM. The threat model concerns memory safety and integrity. While disk usage could be a concern, the specific claim is about memory allocation (OOM). Since the decompressed data is streamed to the multipart parser which offloads large parts to disk, the memory footprint remains bounded. Thus, the claim of unbounded memory allocation leading to OOM is false. The bug pattern (missing limit check) exists, but the consequence (unbounded memory) does not hold because the underlying `multipart.Reader` handles large parts via temp files.\n\nCRUX: Does multipart.Reader.ReadForm allocate unbounded memory for large file parts when the input stream is unlimited?\nGREP: ReadForm",
    "elapsed": 59.7,
    "tokens": 25114,
    "tokens_in": 24593,
    "tokens_out": 521,
    "cost_usd": 0.01189206,
    "file": "http.go",
    "round": 1,
    "grep_used": true,
    "grep_results": "GREP `ReadForm`:\n```\nserver_test.go:342:\tf, err := mr.ReadForm(64)\nhttp_test.go:2862:\tform, err := mr.ReadForm(1024)\nhttp.go:1145:\t\treq.multipartForm, err = mr.ReadForm(8 * 1024)\nhttp.go:1247:\tf, err := mr.ReadForm(int64(maxInMemoryFileSize))\n```"
  },
  {
    "finding_title": "Unbounded gzip decompression in MultipartFormWithLimit(0) streaming path",
    "verdict": "VALID",
    "reasoning": "The report claims that `MultipartFormWithLimit(0)` (the default) leads to unbounded memory allocation when processing a gzip-compressed multipart body stream, causing OOM/DoS. The vulnerability lies in the `bodyStream != nil` path of `MultipartFormWithLimit`. In this path, if `Content-Encoding: gzip` is present, the body stream is wrapped in `gzip.NewReader`. A `LimitedReader` is only applied if `maxBodySize > 0`. With the default `maxBodySize` of 0, no limit is applied to the decompressed stream. This unlimited decompressed stream is then passed to `multipart.NewReader(...).ReadForm(8 * 1024)`. \n\nAlthough `multipart.Reader.ReadForm` streams parts larger than the in-memory limit (8KB) to disk, it still parses the stream in chunks and accumulates the `multipart.FileHeader` and `Value` data in memory. More critically, a 'gzip bomb' can be crafted to decompress into a stream of valid multipart headers or small parts that are all under the 8KB threshold, or simply a stream of data that forces the multipart parser to allocate memory for parsing logic or if the attacker crafts a malformed stream that causes the parser to buffer large amounts of data before failing. Even if the main body is streamed to disk, the lack of a hard stop on the decompression process means an attacker can force the server to decompress and process an arbitrarily large amount of data. While the primary storage for large files is disk, the absence of a limit on the total processed bytes means that the CPU and disk I/O resources are consumed unboundedly. Furthermore, if the attacker crafts a payload where the multipart boundaries are spaced such that parts remain small in memory but the total volume is huge, or if they exploit a parser quirk where it buffers significantly, memory usage can still spike. However, the most definitive issue is that the `gzip.NewReader` decompression itself is not bounded. `gzip.NewReader` allocates buffers as it reads. If the input is a valid gzip stream that decompresses to gigabytes, the `gzip` reader will continue to read and decompress. While `multipart.Reader` handles the output, the `gzip` decompression logic still requires processing. \n\nActually, let's look closer at `multipart.Reader.ReadForm`. It iterates through parts. For each part, if it's a file and larger than the limit, it streams to a temp file. If it's a value, it reads it into memory (capped by the limit? No, `ReadForm`'s limit applies to the file part memory threshold, but values are usually small). \n\nThe key is that `MultipartFormWithLimit(0)` is a public API. An attacker can send a request with `Content-Encoding: gzip` and a `Content-Type: multipart/form-data`. The body is streamed. The library decompresses it. If the decompressed data is huge, the `multipart` parser must process it. Even if it streams to disk, the *resource consumption* (disk space, CPU time, network I/O for reading the decompressed data) is unbounded because there is no `maxBodySize` check on the decompressed stream. The threat model includes \"resource bounds (unbounded allocation, algorithmic complexity, infinite loops on crafted input)\". An unbounded disk write or CPU usage is a valid DoS vector. \n\nFurthermore, `multipart.Reader.ReadForm` returns an error if the multipart stream is malformed. But a valid gzip bomb that decompresses to a valid, extremely large multipart form (e.g., many small fields or one huge file) will be processed. If it's one huge file, it goes to disk. But the server is now writing an arbitrary amount of data to disk, controlled by the attacker (via the decompression ratio). This is a Disk Exhaustion DoS. The threat model explicitly lists \"resource bounds... unbounded allocation\". Writing unbounded data to disk is a resource bound issue. \n\nAdditionally, if the attacker sends a stream that causes the `multipart` parser to accumulate data in memory (e.g. if the boundaries are missing or malformed in a way that causes it to buffer until error), memory could also be exhausted. But the disk exhaustion is sufficient for a DoS. \n\nThe specific claim is \"unbounded memory allocation... leading to OOM\". While `ReadForm` mitigates *memory* exhaustion for large file parts by using temp files, it does not mitigate *disk* exhaustion or *CPU* exhaustion. The report focuses on memory, but the underlying issue (missing limit on decompressed stream) leads to resource exhaustion (disk/CPU) which is a valid security vulnerability under the threat model. However, the report specifically says \"leading to OOM\". Is the OOM claim false? Yes, for the main file content, it goes to disk. But `multipart.Form` holds `Value` fields in memory. An attacker could send a gzip bomb that decompresses to many small multipart fields (values). `ReadForm` stores these in `form.Value`. If there are millions of small fields, `form.Value` will grow unboundedly in memory. This leads to OOM. \n\nExample: A gzip stream that decompresses to: \n`boundary=xyz\r\nContent-Disposition: form-data; name=\"a\"\r\n\r\nA\r\n--xyz\r\n...` repeated millions of times. \nEach field is small, so it stays in memory. The total memory usage grows linearly with the number of fields, which is unbounded. \n\nTherefore, the bug is real. An attacker can trigger it by sending a gzipped multipart body with many small fields (or a large file causing disk exhaustion, but the small fields cause memory exhaustion). The `maxBodySize=0` allows this. \n\nThe prior reviewer dismissed the memory concern because they assumed the main payload is a large file streamed to disk. They missed the vector of many small fields (values) which are kept in memory, or the disk exhaustion vector. \n\nVerdict: VALID. The missing limit allows an attacker to cause unbounded resource consumption (Memory via many small fields, or Disk via large files), leading to DoS.\n\nCRUX: Does an attacker-controlled gzip stream passed to MultipartFormWithLimit(0) allow unbounded accumulation of multipart form values in memory or unbounded disk writes?\nGREP: ReadForm",
    "elapsed": 177.2,
    "tokens": 26729,
    "tokens_in": 25354,
    "tokens_out": 1375,
    "cost_usd": 0.01477368,
    "file": "http.go",
    "round": 2,
    "grep_used": true,
    "grep_results": "GREP `ReadForm`:\n```\nhttp_test.go:2862:\tform, err := mr.ReadForm(1024)\nserver_test.go:342:\tf, err := mr.ReadForm(64)\nhttp.go:1145:\t\treq.multipartForm, err = mr.ReadForm(8 * 1024)\nhttp.go:1247:\tf, err := mr.ReadForm(int64(maxInMemoryFileSize))\n```"
  },
  {
    "finding_title": "Unbounded gzip decompression in MultipartFormWithLimit(0) streaming path",
    "verdict": "VALID",
    "reasoning": "The report identifies a missing size limit in the streaming path of `MultipartFormWithLimit(0)`. When `Content-Encoding: gzip` is present, the body is decompressed via `gzip.NewReader`. If `maxBodySize` is 0, no `io.LimitedReader` is applied to the decompressed stream before passing it to `multipart.NewReader().ReadForm(8 * 1024)`. While `ReadForm` streams large file parts to disk, it stores form *values* (fields) in memory within the `multipart.Form` struct. An attacker can craft a gzip bomb that decompresses to a massive number of small form fields (values), causing unbounded memory allocation and OOM. Additionally, even if parts are streamed to disk, the lack of a limit on total decompressed bytes allows for unbounded disk consumption and CPU exhaustion. Since `MultipartForm()` (the default) calls `MultipartFormWithLimit(0)`, this is reachable by default when handling multipart requests with gzip encoding. The threat model explicitly includes \"unbounded allocation\" and \"resource bounds\" as valid DoS vectors. The absence of a limit here allows an external attacker to exhaust host resources.\n\nCRUX: MultipartFormWithLimit(0) applies no limit to the decompressed gzip stream, allowing unbounded memory/disk usage via crafted input.\nGREP: MultipartFormWithLimit",
    "elapsed": 101.9,
    "tokens": 27083,
    "tokens_in": 26778,
    "tokens_out": 305,
    "cost_usd": 0.01216176,
    "file": "http.go",
    "round": 3,
    "grep_used": true,
    "grep_results": "GREP `MultipartFormWithLimit`:\n```\nREADME.md:382:    For untrusted multipart input, use [ctx.MultipartFormWithLimit()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.MultipartFormWithLimit) (or a custom [Server.FormValueFunc](https://pkg.go.dev/github.com/valyala/fasthttp#Server)) to enforce a parsing size limit.\nserver.go:237:\t// provide a custom FormValueFunc and call MultipartFormWithLimit() there.\nserver.go:1104:// This method is equivalent to MultipartFormWithLimit(0), i.e. no body size\nserver.go:1120:// MultipartFormWithLimit returns request's multipart form and limits the read\nserver.go:1127:func (ctx *RequestCtx) MultipartFormWithLimit(maxBodySize int) (*multipart.Form, error) {\nserver.go:1128:\treturn ctx.Request.MultipartFormWithLimit(maxBodySize)\nserver.go:1140:// For multipart requests with untrusted input, call MultipartFormWithLimit()\nserver.go:1232:// MultipartFormWithLimit() before FormValue or provide a custom\nserver.go:1233:// Server.FormValueFunc that uses MultipartFormWithLimit().\nhttp_test.go:526:func TestRequestMultipartFormWithLimitGzip(t *testing.T) {\nhttp_test.go:547:\t\t_, err := req.MultipartFormWithLimit(len(formBody) - 1)\nhttp_test.go:559:\t\t_, err := req.MultipartFormWithLimit(len(formBody) - 1)\nhttp_test.go:571:\t\tf, err := req.MultipartFormWithLimit(len(formBody))\nhttp.go:1094:// This method is equivalent to MultipartFormWithLimit(0), i.e. no body size\nhttp.go:1100:\treturn req.MultipartFormWithLimit(0)\nhttp.go:1103:// MultipartFormWithLimit returns request's multipart form and limits the\nhttp.go:1113:func (req *Request) MultipartFormWithLimit(maxBodySize int) (*multipart.Form, error) {\n```"
  },
  {
    "finding_title": "Unbounded gzip decompression in MultipartFormWithLimit(0) streaming path",
    "verdict": "VALID",
    "reasoning": "The report claims that `MultipartFormWithLimit(0)` allows unbounded memory allocation via a gzip bomb in the streaming path. I examined the code flow in `http.go`. When `req.bodyStream != nil` and `Content-Encoding: gzip` is present, the body stream is wrapped in `gzip.NewReader`. If `maxBodySize <= 0`, no `io.LimitedReader` is applied to the decompressed stream. This unlimited decompressed stream is then passed to `multipart.NewReader(...).ReadForm(8 * 1024)`. \n\nThe key vulnerability is that `multipart.Reader.ReadForm` stores form **values** (fields that are not files, i.e., `form-data; name=\"key\"`) directly into memory within the `multipart.Form` struct (`f.Value` map). It only streams **file** parts (parts with a `filename` header) to temporary files if they exceed the `maxInMemory` limit (8KB). An attacker can craft a valid gzip stream that decompresses into a multipart body containing millions of small form fields (values). Since these values are not streamed to disk but are instead read into memory and accumulated in the `Form` struct, an attacker can cause unbounded memory consumption (OOM) by sending a small gzipped payload that expands to a huge number of small fields. Additionally, even for large files, the lack of a limit allows unbounded disk usage (Disk Exhaustion DoS). The threat model explicitly includes \"unbounded allocation\" and \"resource bounds\" as valid DoS vectors. The default call `MultipartForm()` uses `maxBodySize=0`, making this reachable by default. The absence of a limit on the decompressed stream is a real security flaw allowing resource exhaustion.\n\nCRUX: Does MultipartFormWithLimit(0) apply a size limit to the decompressed gzip stream before parsing, and does ReadForm store all form data in memory or stream it to disk?\nGREP: ReadForm",
    "elapsed": 230.2,
    "tokens": 27984,
    "tokens_in": 27557,
    "tokens_out": 427,
    "cost_usd": 0.012854939999999999,
    "file": "http.go",
    "round": 4,
    "grep_used": true,
    "grep_results": "GREP `ReadForm`:\n```\nserver_test.go:342:\tf, err := mr.ReadForm(64)\nhttp_test.go:2862:\tform, err := mr.ReadForm(1024)\nhttp.go:1145:\t\treq.multipartForm, err = mr.ReadForm(8 * 1024)\nhttp.go:1247:\tf, err := mr.ReadForm(int64(maxInMemoryFileSize))\n```"
  },
  {
    "finding_title": "Unbounded gzip decompression in MultipartFormWithLimit(0) streaming path",
    "verdict": "INVALID",
    "reasoning": "The report claims that `MultipartFormWithLimit(0)` (the default) allows unbounded memory allocation (OOM) when processing a gzip-compressed multipart body stream because no `io.LimitedReader` is applied to the decompressed stream. I examined the code in `http.go`. Specifically, in the `bodyStream != nil` path, if `Content-Encoding: gzip` is present, the stream is wrapped in `gzip.NewReader`. If `maxBodySize > 0`, an `io.LimitedReader` is applied; otherwise, it is not. The resulting stream is passed to `multipart.NewReader(...).ReadForm(8 * 1024)`. The key question is whether `ReadForm` allocates unbounded memory for a huge decompressed stream. `multipart.Reader.ReadForm` parses the multipart stream. For file parts larger than the `maxInMemory` threshold (8KB in this case), it creates a temporary file and streams the data to it. It does *not* load the entire file content into memory. For value fields (non-file parts), it stores them in the `Form.Value` map. An attacker could theoretically craft a gzip bomb that decompresses to a multipart body with millions of small value fields to cause memory exhaustion, but the primary vector of a 'gzip bomb' (high compression ratio of a single large payload) would typically be a file upload or a large continuous blob. If it's a file, it goes to disk (bounded memory, unbounded disk). If it's a single large value, `ReadForm` would read it into memory until EOF, which could cause OOM. However, the standard threat model for HTTP servers usually relies on `maxMemory` and `maxBodySize` to prevent resource exhaustion. The library provides `MultipartFormWithLimit` specifically to allow setting these bounds. The default behavior of using limit 0 is a convenience feature that places the burden on the application to set limits if they are processing untrusted input with high compression ratios. More importantly, the 'unbounded memory allocation' claim for the *standard* gzip bomb case (large file) is incorrect because it streams to disk. The 'unbounded memory' via millions of small fields is a valid DoS vector but is a secondary interpretation. However, looking at the threat model rules: 'The public API must validate all inputs.' But the API explicitly states `MultipartFormWithLimit(0)` means 'no limit is applied'. It is the user's choice to call the default method. If the user calls `MultipartForm()` (default), they are opting for no limit. Is the library failing to validate? The library *does* allow the user to validate via the `WithLimit` variant. The 'bug' is that the *default* is unsafe for untrusted input with high compression. In security triage, if a library function has an unsafe default that can be easily configured to be safe, and the documentation warns to use the safe variant for untrusted input (as seen in grep results: 'For untrusted multipart input, use ctx.MultipartFormWithLimit()'), this is often considered a 'secure by default' issue or a misconfiguration rather than a flaw in the library's logic itself, unless the 'unsafe' default is unavoidable. Here, it is avoidable. However, the prompt asks if the bug is real and attacker-reachable. The attacker sends a request. The server app (using the library) calls `MultipartForm()`. The library processes it. If the app didn't set a limit, the app is responsible for that choice. The library itself isn't 'buggy' in the sense of ignoring a constraint; it is operating as documented. The 'vulnerability' is in the *application's* decision to use the default limit for untrusted gzip-compressed input. Therefore, marking this as a library vulnerability is invalid; it is a code quality/secure-by-default concern or an application configuration issue. Since the library provides the means to prevent it (the `WithLimit` function) and documents it, the library is not inherently vulnerable in a way that the developer cannot mitigate. The 'unbounded memory' claim for the main body of the request is false due to disk streaming. The 'unbounded memory' via small fields is a valid DoS but depends on the app not setting a limit. Given the library's explicit provision of the limit and documentation, this is not a VALID security vulnerability in the library itself.\n\nCRUX: Does the library force unbounded memory allocation in a way that cannot be mitigated by the application, or does it provide a documented mechanism (MultipartFormWithLimit) to prevent it, making the default unsafe behavior an application configuration issue?\nGREP: MultipartFormWithLimit",
    "elapsed": 98.6,
    "tokens": 28735,
    "tokens_in": 27736,
    "tokens_out": 999,
    "cost_usd": 0.014646119999999999,
    "file": "http.go",
    "round": 5,
    "grep_used": true,
    "grep_results": "GREP `MultipartFormWithLimit`:\n```\nREADME.md:382:    For untrusted multipart input, use [ctx.MultipartFormWithLimit()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.MultipartFormWithLimit) (or a custom [Server.FormValueFunc](https://pkg.go.dev/github.com/valyala/fasthttp#Server)) to enforce a parsing size limit.\nserver.go:237:\t// provide a custom FormValueFunc and call MultipartFormWithLimit() there.\nserver.go:1104:// This method is equivalent to MultipartFormWithLimit(0), i.e. no body size\nserver.go:1120:// MultipartFormWithLimit returns request's multipart form and limits the read\nserver.go:1127:func (ctx *RequestCtx) MultipartFormWithLimit(maxBodySize int) (*multipart.Form, error) {\nserver.go:1128:\treturn ctx.Request.MultipartFormWithLimit(maxBodySize)\nserver.go:1140:// For multipart requests with untrusted input, call MultipartFormWithLimit()\nserver.go:1232:// MultipartFormWithLimit() before FormValue or provide a custom\nserver.go:1233:// Server.FormValueFunc that uses MultipartFormWithLimit().\nhttp_test.go:526:func TestRequestMultipartFormWithLimitGzip(t *testing.T) {\nhttp_test.go:547:\t\t_, err := req.MultipartFormWithLimit(len(formBody) - 1)\nhttp_test.go:559:\t\t_, err := req.MultipartFormWithLimit(len(formBody) - 1)\nhttp_test.go:571:\t\tf, err := req.MultipartFormWithLimit(len(formBody))\nhttp.go:1094:// This method is equivalent to MultipartFormWithLimit(0), i.e. no body size\nhttp.go:1100:\treturn req.MultipartFormWithLimit(0)\nhttp.go:1103:// MultipartFormWithLimit returns request's multipart form and limits the\nhttp.go:1113:func (req *Request) MultipartFormWithLimit(maxBodySize int) (*multipart.Form, error) {\n```\n\nGREP `results:`:\n```\n(no matches in repo)\n```"
  },
  {
    "verdict": "VALID",
    "reasoning": "[ARBITER] The vulnerability exists in the `MultipartFormWithLimit` function within `http.go`. When processing a request with a `bodyStream` (common for large uploads or when using `ReadLimitBody` with streaming) and `Content-Encoding: gzip`, the code wraps the stream in `gzip.NewReader`. If `maxBodySize` is 0 (which is the default when calling `MultipartForm()`, which delegates to `MultipartFormWithLimit(0)`), no `io.LimitedReader` is applied to the decompressed stream. This results in `multipart.NewReader` receiving an unbounded reader. An attacker can send a small gzip-compressed body with a high compression ratio (gzip bomb) that decompresses to a massive size. The `ReadForm` method will then allocate unbounded memory to store the form fields or files, leading to an Out-Of-Memory (OOM) condition and Denial of Service. The library does not enforce a default limit on the decompressed size when the limit is explicitly set to 0 or not provided via the default `MultipartForm()` call.",
    "round": 6,
    "file": "http.go",
    "finding_title": "Unbounded gzip decompression in MultipartFormWithLimit(0) streaming path",
    "elapsed": 140.6,
    "tokens_in": 25387,
    "tokens_out": 243,
    "cost_usd": 0.01139154
  }
]
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."
  }
]
```