Decompression bomb: no output size limit in public decompression API
medium swesweep-fasthttp · compress.go · WriteGunzip / WriteInflate / AppendGunzipBytes / AppendInflateBytes · run #4
Description
All four public decompression functions call writeGunzip/writeInflate with maxBodySize=0 (unlimited). An attacker can supply a small gzip or deflate stream (e.g. a few KB) that decompresses to tens or hundreds of GB, causing unbounded memory allocation via byteSliceWriter.append inside copyZeroAllocWithLimit and crashing the host process with OOM. The internal writeGunzip/writeInflate accept a maxBodySize parameter for limiting, but the public API never exposes it. In a web-server context, any code path that calls AppendGunzipBytes or WriteGunzip on an attacker-controlled request body is vulnerable to a classic decompression-bomb resource-exhaustion attack.
Triage
- Verdict
- VALID confidence 0.83
- Model
- qwen/qwen3.8-27b
- Rounds
- 6
- Cost
- $0.0232 · 416.8 s
[ARBITER] The threat model for a reusable library explicitly classifies 'unbounded allocation' on crafted input as a valid impact category. The code confirms that public decompression functions `WriteGunzip` and `WriteInflate` (and their wrappers `AppendGunzipBytes` and `AppendInflateBytes`) pass `maxBodySize=0` to internal helpers `writeGunzip` and `writeInflate`. In `copyZeroAllocWithLimit`, a limit of 0 typically disables the size check, allowing an attacker to supply a small compressed payload that decompresses to gigabytes, causing OOM crashes in the host process. While Round 5 argued this is a standard design pattern, the binding threat model for this specific library context prioritizes resource exhaustion prevention for public APIs over strict general-purpose utility conventions. Therefore, the lack of a default limit or safe behavior in the public API constitutes a valid vulnerability under the provided rules.
All rounds
[
{
"finding_title": "Decompression bomb: no output size limit in public decompression API",
"verdict": "VALID",
"reasoning": "The reported vulnerability is a decompression bomb (resource exhaustion) in the public decompression APIs `WriteGunzip`, `WriteInflate`, `AppendGunzipBytes`, and `AppendInflateBytes`. The threat model for this library explicitly includes \"unbounded allocation\" and \"resource bounds... on crafted input\" as valid impact categories. The public functions call internal `writeGunzip` and `writeInflate` with `maxBodySize=0`, which disables the limit enforced by `copyZeroAllocWithLimit`. An external attacker (via the host application using the library) can provide a small compressed payload (`p`) that expands to a massive size, causing `byteSliceWriter.append` to allocate unbounded memory and crash the process (OOM). The absence of a limit in the public API is a concrete security flaw because the public boundary does not enforce an invariant to protect the host from resource exhaustion. Since the bug exists, is reachable via public API with untrusted input, and causes meaningful harm (crash/OOM), it is VALID.\n\nCRUX: The public functions `WriteGunzip` and `WriteInflate` pass `maxBodySize=0` to the internal functions, effectively disabling the size limit check in `copyZeroAllocWithLimit`, allowing unbounded memory allocation.\nGREP: return writeGunzip(w, p, 0)",
"elapsed": 7.5,
"tokens": 6196,
"tokens_in": 5904,
"tokens_out": 292,
"cost_usd": 0.0033556799999999998,
"file": "compress.go",
"round": 1,
"grep_used": true,
"grep_results": "GREP `writeGunzip`:\n```\ncompress.go:215:\treturn writeGunzip(w, p, 0)\ncompress.go:218:func writeGunzip(w io.Writer, p []byte, maxBodySize int) (int, error) {\nhttp.go:525:\t_, err := writeGunzip(&bb, p, maxBodySize)\n```"
},
{
"finding_title": "Decompression bomb: no output size limit in public decompression API",
"verdict": "VALID",
"reasoning": "The vulnerability report claims that `WriteGunzip` and `WriteInflate` are vulnerable to decompression bombs because they pass `maxBodySize=0` to the internal helpers, disabling size limits. The grep results confirm that `compress.go:215` does indeed call `writeGunzip(w, p, 0)`. However, the code also reveals a crucial caller at `http.go:525`: `_, err := writeGunzip(&bb, p, maxBodySize)`. This indicates that the internal function `writeGunzip` is also called from the HTTP request handling logic, where a `maxBodySize` limit is passed. \n\nThe threat model defines the attacker as code calling the public API. `WriteGunzip` is a public API. If an application uses `fasthttp.WriteGunzip` directly on untrusted input (e.g., parsing a compressed header or body outside the standard request/response flow), it is vulnerable. More importantly, the standard fasthttp request handling path (in `http.go`) enforces limits by passing `maxBodySize`. The vulnerability specifically targets the *public utility functions* in `compress.go` which lack this protection. \n\nSince `WriteGunzip` is exported and intended for use by library consumers, and it accepts an attacker-controlled payload `p` that can be decompressed into unbounded memory (as verified by the `maxBodySize=0` argument), this constitutes a valid resource exhaustion vulnerability for any host application that uses this function on untrusted data without implementing their own limits. The existence of a safe internal path (`http.go`) does not mitigate the risk for the public API surface which explicitly lacks the bound. The absence of a default limit in the public API is a design flaw that allows attackers to trigger OOM by crafting a high-compression-ratio payload.\n\nCRUX: Whether the public API function `WriteGunzip` imposes a size limit on the decompressed output, which determines if an attacker can cause unbounded memory allocation.\nGREP: func WriteGunzip",
"elapsed": 179.7,
"tokens": 6856,
"tokens_in": 6417,
"tokens_out": 439,
"cost_usd": 0.00401214,
"file": "compress.go",
"round": 2,
"grep_used": true,
"grep_results": "GREP `func WriteGunzip`:\n```\ncompress.go:214:func WriteGunzip(w io.Writer, p []byte) (int, error) {\n```"
},
{
"finding_title": "Decompression bomb: no output size limit in public decompression API",
"verdict": "VALID",
"reasoning": "The report correctly identifies that the public API functions `WriteGunzip` and `WriteInflate` pass `maxBodySize=0` to their internal helpers, effectively disabling any size limit on the decompressed output. While the internal HTTP server implementation (e.g., in `http.go`) correctly enforces `maxBodySize` limits, the threat model for a library explicitly states that 'Public (exported) functions... must validate all inputs.' When a host application uses `fasthttp.WriteGunzip` or `AppendGunzipBytes` to process untrusted data (such as a payload from a different source, or a misconfigured endpoint), they are relying on the library to be safe. Without a default limit, a small crafted payload can expand to consume all available memory (OOM), crashing the host process. Since the public API lacks this defense, and the input is attacker-controlled, this is a valid resource exhaustion vulnerability.\n\nCRUX: The public function `WriteGunzip` calls the internal `writeGunzip` with `maxBodySize=0`, removing the protection against decompression bombs that exists in the internal HTTP server path.\nGREP: return writeGunzip(w, p, 0)",
"elapsed": 75.9,
"tokens": 7144,
"tokens_in": 6883,
"tokens_out": 261,
"cost_usd": 0.0036738599999999997,
"file": "compress.go",
"round": 3,
"grep_used": true,
"grep_results": "GREP `writeGunzip`:\n```\ncompress.go:215:\treturn writeGunzip(w, p, 0)\ncompress.go:218:func writeGunzip(w io.Writer, p []byte, maxBodySize int) (int, error) {\nhttp.go:525:\t_, err := writeGunzip(&bb, p, maxBodySize)\n```"
},
{
"finding_title": "Decompression bomb: no output size limit in public decompression API",
"verdict": "VALID",
"reasoning": "The vulnerability report claims that the public decompression APIs (`WriteGunzip`, `AppendGunzipBytes`, etc.) are vulnerable to decompression bombs because they pass `maxBodySize=0` to internal helpers, disabling size limits. I have verified the following: \n\n1. **Bug Pattern Existence**: `grep` confirms that `WriteGunzip` and `WriteInflate` in `compress.go` indeed call `writeGunzip(w, p, 0)` and `writeInflate(w, p, 0)`. The value `0` for `maxBodySize` indicates no limit (standard convention in this codebase, likely checking `if maxBodySize > 0` in `copyZeroAllocWithLimit`).\n\n2. **Attacker Reachability**: `WriteGunzip` and `AppendGunzipBytes` are public (exported) functions. The threat model for a library states that the attacker is code calling the public API, and these functions accept `p []byte` (or `src []byte`) which can be attacker-controlled data (e.g., from a network request in a host application). There is no authentication or sanitization at the library boundary for these utility functions.\n\n3. **Absence of Defense**: I checked for a defense. The internal HTTP path (`http.go`) uses a limit, but the public API path does not. There is no default limit imposed by the public functions themselves. The `byteSliceWriter` simply appends to a slice, growing as the decompressor outputs data. A gzip/deflate stream can have a high compression ratio (e.g., 1:10,000 or higher for highly repetitive data), meaning a small input (e.g., 1KB) can expand to 10GB, exhausting memory.\n\n4. **Security Relevance**: Resource exhaustion (OOM crash) is a valid security impact under the threat model (\"resource bounds... unbounded allocation\"). A library utility function that allows unbounded memory growth on attacker-controlled input is a security vulnerability for any host application that uses it without implementing its own strict limits. The library's responsibility to be safe on public surfaces (especially for parsers/decoders) makes this a valid finding.\n\nPrior reviewers correctly identified the issue. I concur with the VALID verdict. The crux is the lack of a default size limit in the public API, which allows an attacker to trigger OOM via a crafted compressed payload.\n\nCRUX: The public functions `WriteGunzip` and `WriteInflate` pass `maxBodySize=0`, disabling the size limit and allowing unbounded memory allocation from a small compressed input.\nGREP: return writeGunzip(w, p, 0)",
"elapsed": 135.6,
"tokens": 7801,
"tokens_in": 7218,
"tokens_out": 583,
"cost_usd": 0.00478056,
"file": "compress.go",
"round": 4,
"grep_used": true,
"grep_results": "GREP `writeGunzip`:\n```\ncompress.go:215:\treturn writeGunzip(w, p, 0)\ncompress.go:218:func writeGunzip(w io.Writer, p []byte, maxBodySize int) (int, error) {\nhttp.go:525:\t_, err := writeGunzip(&bb, p, maxBodySize)\n```"
},
{
"finding_title": "Decompression bomb: no output size limit in public decompression API",
"verdict": "INVALID",
"reasoning": "The reported vulnerability claims that the public decompression APIs (`WriteGunzip`, `WriteInflate`, etc.) are vulnerable to decompression bombs because they pass `maxBodySize=0` to internal helpers, allowing unbounded memory allocation. While the code analysis confirms that `maxBodySize=0` disables the limit in `copyZeroAllocWithLimit`, this specific pattern is a standard design choice for general-purpose compression libraries (like zlib, gzip, etc.). The threat model for a reusable library specifies that the attacker is code calling the public API. In this context, the host application is responsible for managing resource limits based on its own security requirements. The library provides the mechanism (`maxBodySize` parameter in internal functions, and the fact that `copyZeroAllocWithLimit` supports it), but leaving the limit configurable (or unset by default) is not a security vulnerability in the library itself; it is the responsibility of the host application to set appropriate limits when processing untrusted input. Furthermore, the internal HTTP server path (`http.go:525`) correctly enforces limits, showing that the library's primary server implementation is safe. Marking the public utility functions as vulnerable because they don't enforce a default limit imposes an arbitrary policy on the library that is not a universal security requirement. Therefore, this is a code quality/design preference rather than a security vulnerability exploitable by an attacker to compromise the library's integrity in a way that the library is at fault.\n\nCRUX: Whether the lack of a default size limit in public utility functions of a library constitutes a security vulnerability when the host application is responsible for setting limits, versus a standard design pattern for general-purpose compression tools.\nGREP: func WriteGunzip",
"elapsed": 9.5,
"tokens": 8234,
"tokens_in": 7871,
"tokens_out": 363,
"cost_usd": 0.00439482,
"file": "compress.go",
"round": 5,
"grep_used": true,
"grep_results": "GREP `func WriteGunzip`:\n```\ncompress.go:214:func WriteGunzip(w io.Writer, p []byte) (int, error) {\n```"
},
{
"verdict": "VALID",
"reasoning": "[ARBITER] The threat model for a reusable library explicitly classifies 'unbounded allocation' on crafted input as a valid impact category. The code confirms that public decompression functions `WriteGunzip` and `WriteInflate` (and their wrappers `AppendGunzipBytes` and `AppendInflateBytes`) pass `maxBodySize=0` to internal helpers `writeGunzip` and `writeInflate`. In `copyZeroAllocWithLimit`, a limit of 0 typically disables the size check, allowing an attacker to supply a small compressed payload that decompresses to gigabytes, causing OOM crashes in the host process. While Round 5 argued this is a standard design pattern, the binding threat model for this specific library context prioritizes resource exhaustion prevention for public APIs over strict general-purpose utility conventions. Therefore, the lack of a default limit or safe behavior in the public API constitutes a valid vulnerability under the provided rules.",
"round": 6,
"file": "compress.go",
"finding_title": "Decompression bomb: no output size limit in public decompression API",
"elapsed": 8.6,
"tokens_in": 5747,
"tokens_out": 205,
"cost_usd": 0.00302874
}
]Raw scan report for this file
## Security Analysis: `compress.go` (fasthttp)
This file is fasthttp's compression helper layer, providing gzip/deflate compression and decompression utilities. The public API (`WriteGzip*`, `WriteGunzip`, `WriteInflate*`, `Append*`) accepts attacker-controlled data (request/response bodies, file contents) and passes it through compression/decompression codecs.
### Function-by-Function Trace
**Decompression path (highest risk):** `WriteGunzip(w, p)` → `writeGunzip(w, p, 0)` → `byteSliceReader{b: p}` → `gzip.NewReader(r)` → `copyZeroAllocWithLimit(w, zr, 0)`. The `maxBodySize=0` means **no output size cap**. An attacker-supplied `p` (a few hundred bytes of highly compressed data) can expand to gigabytes of output, allocating via `byteSliceWriter.b = append(w.b, p...)` inside `copyZeroAllocWithLimit`. The `AppendGunzipBytes`/`AppendInflateBytes` variants make this worse: the decompressed output is appended to `dst` with no limit, and the resulting slice is returned to the caller.
**Compression path:** `WriteGzipLevel(w, p, level)` → for non-blocking writer types (`*byteSliceWriter`, `*bytes.Buffer`, `*bytebufferpool.ByteBuffer`), the level is clamped by `normalizeCompressLevel` into `[0..11]` for pool indexing, and the actual compression is done in `nonblockingWriteGzip` via a pooled `gzip.Writer`. The level passed to `gzip.NewWriterLevel` is the raw `level`, but `acquireRealGzipWriter` clamps invalid levels before the second attempt. No buffer overflow or out-of-bounds index is possible.
**Pool invariants:** `sync.Pool` entries are always the correct type because they are only populated by the corresponding `acquire*`/`release*` pairs. The interface assertions (`v.(*gzip.Reader)`, `ctxv.(*compressCtx)`, `zr.(zlib.Resetter)`) are safe under normal operation.
**`byteSliceReader` / `byteSliceWriter`:** Both are trivially safe. `Read` copies `min(len(p), len(r.b))` bytes; `Write` uses `append` which handles growth. No fixed-size buffers.
**`isFileCompressible`:** Uses `io.LimitedReader{N: 4096}` to bound reads. The `n := 4096 - lr.N` computation is safe (result ∈ [0, 4096]). The ignored `Seek` error is benign — worst case the compressibility check is slightly inaccurate. A nil `f` would panic, but this is an internal function and the caller validates file opens.
---
### Findings
```json
[
{
"severity": "medium",
"title": "Decompression bomb: no output size limit in public decompression API",
"function": "WriteGunzip / WriteInflate / AppendGunzipBytes / AppendInflateBytes",
"description": "All four public decompression functions call writeGunzip/writeInflate with maxBodySize=0 (unlimited). An attacker can supply a small gzip or deflate stream (e.g. a few KB) that decompresses to tens or hundreds of GB, causing unbounded memory allocation via byteSliceWriter.append inside copyZeroAllocWithLimit and crashing the host process with OOM. The internal writeGunzip/writeInflate accept a maxBodySize parameter for limiting, but the public API never exposes it. In a web-server context, any code path that calls AppendGunzipBytes or WriteGunzip on an attacker-controlled request body is vulnerable to a classic decompression-bomb resource-exhaustion attack."
},
{
"severity": "low",
"title": "WriteGzipLevel / WriteDeflateLevel return input length instead of compressed byte count for non-blocking writers",
"function": "WriteGzipLevel / WriteDeflateLevel",
"description": "When w is a *byteSliceWriter, *bytes.Buffer, or *bytebufferpool.ByteBuffer, the function synchronously compresses p into w via stacklessWriteGzip/stacklessWriteDeflate, but returns len(p), nil — the size of the uncompressed input, not the number of compressed bytes actually written. The API contract states it returns 'the number of compressed bytes written to w'. A caller that uses this return value to slice, buffer, or frame the compressed output (e.g. buf.Bytes()[:n]) will read too many or too few bytes, producing a malformed compressed payload on the wire. This is a correctness bug that could cause protocol desynchronisation or data corruption in dependent code, though it is not a direct memory-safety issue in Go."
},
{
"severity": "low",
"title": "Ignored error on fallback compression writer creation can yield nil writer",
"function": "acquireRealGzipWriter / acquireRealDeflateWriter",
"description": "When gzip.NewWriterLevel / zlib.NewWriterLevel fails on the first attempt (invalid level), the code clamps the level and retries, but discards the second error: 'zw, _ = gzip.NewWriterLevel(w, level)'. If the retry also fails (e.g. a future library change adds a new failure mode), zw is nil. The corresponding release function (releaseRealGzipWriter / releaseRealDeflateWriter) then calls zw.Close() on a nil pointer, causing a panic and process crash. This path is currently unreachable because the clamped level is always valid, but the unchecked error is a latent nil-pointer dereference."
}
]
```