Out-of-range compression level is passed to brotli.NewWriterLevel and pooled under a normalized key
medium swesweep-fasthttp · brotli.go · acquireRealBrotliWriter · run #4
Description
WriteBrotliLevel and AppendBrotliBytesLevel accept an arbitrary int level. normalizeBrotliCompressLevel only bounds the value for pool indexing, but acquireRealBrotliWriter passes the original level to brotli.NewWriterLevel. If the brotli dependency expects levels in [0..11], values such as -1 or 12 can cause a panic in the host process. Even if the dependency clamps the level, writers created with out-of-range levels are stored under the normalized default pool bucket, and Reset does not change their configured level. Later legitimate requests for the default level can reuse a writer configured for a different level, causing incorrect compression behavior or further undefined behavior.
Triage
- Verdict
- INVALID confidence 0.0
- Model
- qwen/qwen3.8-27b
- Rounds
- 5
- Cost
- $0.0166 · 552.9 s
The reported vulnerability claims that an out-of-range `level` parameter passed to `WriteBrotliLevel` leads to a panic or incorrect behavior because the raw level is used for writer creation, while only the pool index is normalized. While the code flaw exists (mismatch between validation for pooling and validation for usage), the security relevance is determined by the threat model. The threat model defines the attacker as an external entity providing untrusted data (payloads, headers) to the host application. The `level` parameter is an integer configuration value set by the trusted host application code (the library user/developer), typically derived from server configuration or hardcoded constants. It is not parsed from untrusted network input (such as request bodies or headers) within the library's logic. Therefore, an external attacker cannot control the `level` value to trigger the out-of-range condition. Passing an invalid level constitutes a violation of the public API's documented preconditions by the trusted caller, which the threat model explicitly excludes from being a vulnerability. Thus, this is a code robustness issue, not a security vulnerability. CRUX: The `level` parameter is controlled by the trusted host application, not by untrusted external input. GREP: func WriteBrotliLevel
All rounds
[
{
"finding_title": "Out-of-range compression level is passed to brotli.NewWriterLevel and pooled under a normalized key",
"verdict": "INVALID",
"reasoning": "The reported vulnerability claims that passing an out-of-range level (e.g., -1 or 12) to `WriteBrotliLevel` causes a panic because the original level is passed to `brotli.NewWriterLevel` after being normalized only for pool indexing. \n\n1. **Bug Pattern Analysis**: In `acquireRealBrotliWriter` (brotli.go:58), if a pooled writer is not available, `brotli.NewWriterLevel(w, level)` is called with the *original* unvalidated `level`. However, if a writer *is* available from the pool, `zw.Reset(w)` is called, which reuses the existing writer configuration. The pool key is `normalizeBrotliCompressLevel(level)`, which clamps out-of-range values to `CompressBrotliDefaultCompression` (4). \n - If a user passes `level=12`, it is normalized to 4 for the pool key. \n - If the pool for level 4 is empty, a new writer is created with `level=12`. \n - If the pool for level 4 has an existing writer (created with valid level 4), `Reset` is called, and the compression level remains 4. \n\n2. **Attacker Reachability**: The `level` parameter in `WriteBrotliLevel` is an `int`. In a typical HTTP server usage, the compression level is configured by the application developer (trusted) via `Server.CompressionLevel` or similar settings, not by the client. The threat model states: \"Attacker: Code that calls the public API... parsers, decoders... receive adversarial payloads\". It further states: \"Not a vulnerability under this model: ... misuse that violates a documented precondition of the public API.\" The documented supported levels are 0-11. Passing an integer outside this range is a misuse of the API by the application developer. The attacker (client) does not control the `level` integer passed to `WriteBrotliLevel`; they only control the `src`/`p` byte arrays. Therefore, the input that triggers the potential panic (the level integer) is not attacker-controlled in the context of the threat model where the host application is trusted to call the API correctly with valid levels. \n\n3. **Defense Verification**: Even if an attacker could control the level, the brotli library might handle it, but the critical point is the threat model. The level is a configuration parameter, not a data payload. The threat model explicitly excludes issues where the attacker does not control the specific input violating the invariant. Since the `level` is set by the host application code (trusted), and not parsed from the network payload, it is not an attacker-reachable bug in the sense of the defined threat model. \n\n4. **Conclusion**: The bug (potential panic or incorrect behavior due to unvalidated level) is a code quality/robustness issue for the library user. It is not a security vulnerability reachable by an external attacker via untrusted network input, because the `level` parameter is not derived from untrusted data. The attacker controls `src`, not `level`.\n\nCRUX: The `level` parameter in `WriteBrotliLevel` is controlled by the application developer (trusted code), not by the external attacker (untrusted network input), so the precondition violation is not attacker-reachable.\nGREP: func WriteBrotliLevel",
"elapsed": 98.9,
"tokens": 4543,
"tokens_in": 3798,
"tokens_out": 745,
"cost_usd": 0.00383016,
"file": "brotli.go",
"round": 1,
"grep_used": true,
"grep_results": "GREP `func WriteBrotliLevel`:\n```\nbrotli.go:114:func WriteBrotliLevel(w io.Writer, p []byte, level int) (int, error) {\n```"
},
{
"finding_title": "Out-of-range compression level is passed to brotli.NewWriterLevel and pooled under a normalized key",
"verdict": "INVALID",
"reasoning": "The report claims that passing an out-of-range compression level (e.g., -1, 12) to `WriteBrotliLevel` or `AppendBrotliBytesLevel` will cause a panic or incorrect behavior because the original level is passed to `brotli.NewWriterLevel` while only the pool index is normalized. \n\n1. **Bug Verification**: The code analysis is correct. `normalizeBrotliCompressLevel` clamps the value for the pool map key, but `acquireRealBrotliWriter` and `stacklessWriteBrotli` pass the raw `level` to `brotli.NewWriterLevel`. If `level` is outside the range accepted by the brotli dependency (typically 0-11), this could lead to undefined behavior, a panic, or incorrect compression states stored in the pool (mixing configurations). \n\n2. **Threat Model & Reachability**: The threat model defines the attacker as an external entity sending untrusted data (payloads, headers) to the host application. The host application is trusted to configure the library correctly. The `level` parameter in `WriteBrotliLevel(w, p, level)` is an integer provided by the application developer when calling the public API. It is **not** parsed from the network request (unlike `p` or `w` contents). \n\n3. **Conclusion**: Since the attacker (client) cannot control the `level` integer passed to the function, they cannot trigger the out-of-range condition. Passing an invalid level is a misuse of the API by the trusted host application, which violates the threat model's definition of a vulnerability (where the attacker must control the input triggering the bug). Therefore, this is a code quality/robustness issue for the library user, not a security vulnerability exploitable by an external attacker.\n\nCRUX: The `level` parameter is controlled by the trusted host application code, not by the external attacker's untrusted network input.\nGREP: func WriteBrotliLevel",
"elapsed": 47.5,
"tokens": 5153,
"tokens_in": 4712,
"tokens_out": 441,
"cost_usd": 0.00330204,
"file": "brotli.go",
"round": 2,
"grep_used": true,
"grep_results": "GREP `func WriteBrotliLevel`:\n```\nbrotli.go:114:func WriteBrotliLevel(w io.Writer, p []byte, level int) (int, error) {\n```"
},
{
"finding_title": "Out-of-range compression level is passed to brotli.NewWriterLevel and pooled under a normalized key",
"verdict": "INVALID",
"reasoning": "The reported vulnerability hinges on an attacker passing an out-of-range `int` value to the `level` parameter of `WriteBrotliLevel` or `AppendBrotliBytesLevel`. The threat model explicitly defines the attacker as an external entity sending untrusted data (HTTP payloads, headers) to the host application, while the host application itself is considered trusted code. The `level` parameter is a configuration integer chosen by the application developer (e.g., hardcoded as `CompressBrotliDefaultCompression` or set via server configuration) and is not derived from or controllable by the client's network input. While `acquireRealBrotliWriter` does pass the raw `level` to `brotli.NewWriterLevel`, this code path is only triggered if the trusted host application explicitly passes an invalid level, which constitutes a violation of the API's documented preconditions (supported levels 0-11). Since the attacker cannot control the `level` parameter through the untrusted entry points (data payloads), this is not an externally reachable security vulnerability under the provided threat model. It is a robustness issue for the library user.\n\nCRUX: The `level` parameter is controlled by the trusted host application code, not by the external attacker's untrusted network input.\nGREP: func WriteBrotliLevel",
"elapsed": 59.9,
"tokens": 5467,
"tokens_in": 5180,
"tokens_out": 287,
"cost_usd": 0.0030365999999999995,
"file": "brotli.go",
"round": 3,
"grep_used": true,
"grep_results": "GREP `func WriteBrotliLevel`:\n```\nbrotli.go:114:func WriteBrotliLevel(w io.Writer, p []byte, level int) (int, error) {\n```"
},
{
"finding_title": "Out-of-range compression level is passed to brotli.NewWriterLevel and pooled under a normalized key",
"verdict": "INVALID",
"reasoning": "The report claims that an out-of-range compression level passed to `WriteBrotliLevel` can cause a panic or incorrect behavior because the raw level is passed to `brotli.NewWriterLevel` while only the pool index is normalized. However, the threat model for this library explicitly defines the attacker as an external entity providing untrusted data (payloads, headers) to the host application, while the host application is considered trusted code. The `level` parameter is an integer configuration value determined by the application developer (e.g., set via `Server.CompressionLevel` or hardcoded constants), not a value parsed from untrusted network input like an HTTP request body or header. Since the attacker does not control the `level` argument, they cannot trigger the out-of-range condition by sending malicious data. Passing an invalid level constitutes a misuse of the public API by the trusted host application, which violates the documented preconditions (levels 0-11) but is not an externally reachable security vulnerability under the specified threat model. Therefore, this is a code quality/robustness issue for the library user, not a security vulnerability exploitable by an external attacker.\n\nCRUX: The `level` parameter is controlled by the trusted host application code, not by the external attacker's untrusted network input.\nGREP: func WriteBrotliLevel",
"elapsed": 313.8,
"tokens": 5789,
"tokens_in": 5500,
"tokens_out": 289,
"cost_usd": 0.003177,
"file": "brotli.go",
"round": 4,
"grep_used": true,
"grep_results": "GREP `func WriteBrotliLevel`:\n```\nbrotli.go:114:func WriteBrotliLevel(w io.Writer, p []byte, level int) (int, error) {\n```"
},
{
"finding_title": "Out-of-range compression level is passed to brotli.NewWriterLevel and pooled under a normalized key",
"verdict": "INVALID",
"reasoning": "The reported vulnerability claims that an out-of-range `level` parameter passed to `WriteBrotliLevel` leads to a panic or incorrect behavior because the raw level is used for writer creation, while only the pool index is normalized. While the code flaw exists (mismatch between validation for pooling and validation for usage), the security relevance is determined by the threat model. The threat model defines the attacker as an external entity providing untrusted data (payloads, headers) to the host application. The `level` parameter is an integer configuration value set by the trusted host application code (the library user/developer), typically derived from server configuration or hardcoded constants. It is not parsed from untrusted network input (such as request bodies or headers) within the library's logic. Therefore, an external attacker cannot control the `level` value to trigger the out-of-range condition. Passing an invalid level constitutes a violation of the public API's documented preconditions by the trusted caller, which the threat model explicitly excludes from being a vulnerability. Thus, this is a code robustness issue, not a security vulnerability.\n\nCRUX: The `level` parameter is controlled by the trusted host application, not by untrusted external input.\nGREP: func WriteBrotliLevel",
"elapsed": 32.8,
"tokens": 6092,
"tokens_in": 5822,
"tokens_out": 270,
"cost_usd": 0.0032552399999999995,
"file": "brotli.go",
"round": 5,
"grep_used": true,
"grep_results": "GREP `func WriteBrotliLevel`:\n```\nbrotli.go:114:func WriteBrotliLevel(w io.Writer, p []byte, level int) (int, error) {\n```"
}
]Raw scan report for this file
## Security analysis of `brotli.go`
This file exposes public brotli compression/decompression helpers for `fasthttp`. The trust boundary is the public API:
- `AppendBrotliBytesLevel(dst, src, level)`
- `WriteBrotliLevel(w, p, level)`
- `WriteBrotli(w, p)`
- `AppendBrotliBytes(dst, src)`
- `WriteUnbrotli(w, p)`
- `AppendUnbrotliBytes(dst, src)`
The main risks are:
1. **Unbounded decompression / decompression bomb**
- `WriteUnbrotli` calls the internal `writeUnbrotli(w, p, 0)`.
- `writeUnbrotli` passes `maxBodySize = 0` to `copyZeroAllocWithLimit(w, zr, maxBodySize)`.
- In `fasthttp`, `0` normally means “no limit”.
- Therefore, a small malicious brotli payload can expand to a very large amount of output.
- `AppendUnbrotliBytes` makes this worse because it wraps `dst` in `byteSliceWriter`, so the decompressed output is appended to `dst` with no external cap.
- The later check:
```go
nn := int(n)
if int64(nn) != n {
return 0, fmt.Errorf("too much data unbrotlied: %d", n)
}
```
only helps on 32-bit platforms and only after the copy has already happened. On 64-bit platforms it does not impose a limit.
- Impact: remote denial of service through memory exhaustion when an untrusted brotli payload is decompressed.
2. **Unvalidated compression level passed to the brotli writer**
- Public functions accept `level int` directly.
- `normalizeBrotliCompressLevel` clamps the value only for use as a pool index:
```go
if level < 0 || level > 11 {
level = CompressBrotliDefaultCompression
}
```
- However, `acquireRealBrotliWriter` passes the **original** `level` to `brotli.NewWriterLevel(w, level)`.
- If the dependency assumes brotli levels are `[0..11]`, out-of-range values such as `-1` or `12` can cause a panic, for example by indexing a level table outside bounds.
- Even if the dependency clamps the level internally, the pool key is normalized while the actual writer state is not. Writers created with out-of-range levels can be stored under the default pool bucket and later reused for a legitimate level such as `4`.
- This can lead to:
- process crash / panic,
- incorrect compression behavior,
- cross-contamination of pooled writers.
3. **Ignored stackless deferred-execution return value can cause a data race**
- For non-blocking writer types:
```go
case *byteSliceWriter,
*bytes.Buffer,
*bytebufferpool.ByteBuffer:
```
`WriteBrotliLevel` uses `stacklessWriteBrotli(ctx)`.
- `stacklessWriteBrotli` does this:
```go
stacklessWriteBrotliFunc(ctx)
```
and ignores the returned `bool`.
- In `fasthttp/stackless`, that boolean generally indicates whether the function ran synchronously in the current goroutine or whether execution was deferred to another goroutine.
- If the call is deferred, `WriteBrotliLevel` returns before the compressed data has actually been written.
- For `AppendBrotliBytesLevel`, that means `dst` can be returned to the caller before the brotli writer has appended the compressed bytes.
- This creates a data race on:
- `dst` / `byteSliceWriter.b`,
- possibly `src` / `p`,
- internal writer state.
- It can also cause:
- incomplete compressed output,
- corrupted compressed output,
- lost write errors,
- incorrect returned byte count.
- This is especially important because `AppendBrotliBytesLevel` is a public API that returns the final buffer.
4. **Incorrect returned byte count for non-blocking writers**
- For the non-blocking writer cases, `WriteBrotliLevel` returns:
```go
return len(p), nil
```
- The documentation says the function returns “the number of compressed bytes written to `w`”.
- `len(p)` is the **uncompressed input length**, not the compressed output length.
- If a caller uses this return value to set `Content-Length`, manage response buffers, or perform protocol framing, it may produce incorrect lengths or protocol desynchronization.
- This is lower severity than the previous issues, but it is still a public API correctness problem with security relevance.
## Areas reviewed and deprioritized
- **Pool type assertions** such as `v.(*brotli.Reader)`, `v.(*brotli.Writer)`, and `v.(stackless.Writer)` are internal. The pools are only populated by internal release functions, so they are not directly attacker-controllable.
- **Nil `p` or nil `dst`** are mostly safe:
- `p == nil` compresses/decompresses empty input.
- `dst == nil` works with `byteSliceWriter` because `append` can start from `nil`.
- **Nil `w`** can panic, but that is primarily a caller-error issue rather than a typical untrusted-data issue.
- **Ignored compression errors** in `nonblockingWriteBrotli` are real, but for the specific non-blocking writer types used here the practical impact is mostly correctness/data-race related rather than a direct memory-safety bug.
- There are **no fixed-size stack buffers** in this file, so there is no classic C-style buffer overflow.
```json
[
{
"severity": "high",
"title": "Unbounded brotli decompression can exhaust memory",
"function": "WriteUnbrotli",
"description": "WriteUnbrotli calls writeUnbrotli with maxBodySize=0, and writeUnbrotli passes that value to copyZeroAllocWithLimit. In fasthttp, 0 normally disables the limit, so an attacker-controlled brotli payload can decompress to arbitrarily large output. AppendUnbrotliBytes makes this exploitable as a decompression bomb because it appends the decompressed data to dst without any cap. The int overflow check in writeUnbrotli runs after the copy has already written data and is only effective on 32-bit platforms, so it does not prevent denial of service via memory exhaustion."
},
{
"severity": "medium",
"title": "Out-of-range compression level is passed to brotli.NewWriterLevel and pooled under a normalized key",
"function": "acquireRealBrotliWriter",
"description": "WriteBrotliLevel and AppendBrotliBytesLevel accept an arbitrary int level. normalizeBrotliCompressLevel only bounds the value for pool indexing, but acquireRealBrotliWriter passes the original level to brotli.NewWriterLevel. If the brotli dependency expects levels in [0..11], values such as -1 or 12 can cause a panic in the host process. Even if the dependency clamps the level, writers created with out-of-range levels are stored under the normalized default pool bucket, and Reset does not change their configured level. Later legitimate requests for the default level can reuse a writer configured for a different level, causing incorrect compression behavior or further undefined behavior."
},
{
"severity": "high",
"title": "Ignored stackless deferred-execution return value can cause a data race in brotli compression",
"function": "stacklessWriteBrotli",
"description": "stacklessWriteBrotli ignores the bool returned by stacklessWriteBrotliFunc. In the fasthttp stackless runtime, this return value indicates whether nonblockingWriteBrotli executed synchronously in the current goroutine or was deferred to another goroutine. If execution is deferred, WriteBrotliLevel returns len(p), nil and AppendBrotliBytesLevel returns dst before the compressed bytes have been written. This creates a data race on dst/src and internal writer state, can return incomplete or corrupted compressed output, and hides write errors. The race is reachable when compression is invoked with a stack near the limit or from a deep call stack."
},
{
"severity": "low",
"title": "WriteBrotliLevel returns uncompressed input length instead of compressed byte count for non-blocking writers",
"function": "WriteBrotliLevel",
"description": "For *byteSliceWriter, *bytes.Buffer, and *bytebufferpool.ByteBuffer, WriteBrotliLevel returns len(p), nil. That value is the uncompressed input length, not the number of compressed bytes written to the writer, contrary to the documented contract. Callers that use the returned value to set Content-Length, manage response buffers, or perform protocol framing may produce incorrect lengths or misuse memory, which can lead to protocol desynchronization or resource handling bugs."
}
]
```