BudgetScan

Missing io.Writer validation can panic with nil or typed-nil writers

low swesweep-fasthttp · zstd.go · WriteZstdLevel/WriteUnzstd · run #4

Description

The exported zstd helpers do not validate w. A nil or typed-nil io.Writer can reach zstd.NewWriter in acquireRealZstdWriter, where an error is handled with panic(err), or can be dereferenced by copyZeroAllocWithLimit when writing decompressed data. A caller can trigger a crash by supplying such a writer.

Triage

Not triaged.

Raw scan report for this file

The most important flow is decompression:

- `WriteUnzstd(w, p)` calls `writeUnzstd(w, p, 0)`.
- `AppendUnzstdBytes(dst, src)` also reaches `WriteUnzstd` with `maxBodySize=0`.
- `writeUnzstd` creates a `zstd.Decoder` over attacker-controlled `p` and copies output to `w` via `copyZeroAllocWithLimit(w, zr, maxBodySize)`.
- With `maxBodySize=0`, the copy limit is effectively unlimited in fasthttp’s helper semantics.
- The `zstd.Decoder` is not configured with an output/memory cap.

This allows a small malicious zstd stream to expand to an arbitrarily large decompressed body, causing unbounded memory allocation and OOM in the host process. This is the primary external-attacker-triggered issue.

Secondary issues:

- The exported zstd helpers do not validate `w io.Writer`. A `nil` or typed-nil writer can reach `zstd.NewWriter` in `acquireRealZstdWriter`, where an error is handled with `panic(err)`, or can be dereferenced during decompression in `copyZeroAllocWithLimit`. This is mainly a robustness/DoS issue.
- The fast-path compression path ignores zstd encoder write errors. `nonblockingWriteZstd` discards `zw.Write` errors, and `AppendZstdBytesLevel` ignores the return of `WriteZstdLevel`. This can hide failures when a custom `io.Writer` is supplied, but is lower severity for normal fasthttp usage with concrete buffer writers.

Other considered areas appear safe or low-value:

- `normalizeZstdCompressLevel` bounds level to `0..4`, so pool indexing is not out-of-range based on the constants in this file.
- Pooled `zstd.Decoder` / `zstd.Encoder` reuse uses `Reset`/`Close`, so there is no obvious use-after-free or stale-writer bug.
- Malformed zstd input should surface as an error from the `zstd` package rather than a panic in this code.

```json
[
  {
    "severity": "high",
    "title": "Unbounded zstd decompression enables remote DoS via decompression bomb",
    "function": "WriteUnzstd",
    "description": "WriteUnzstd and AppendUnzstdBytes route attacker-controlled zstd data through writeUnzstd with maxBodySize=0. copyZeroAllocWithLimit uses 0 as unlimited, and the zstd.Decoder is created without an output/memory cap. A small malicious zstd stream can expand to an arbitrarily large decompressed body, causing unbounded memory allocation and OOM in the host process."
  },
  {
    "severity": "low",
    "title": "Missing io.Writer validation can panic with nil or typed-nil writers",
    "function": "WriteZstdLevel/WriteUnzstd",
    "description": "The exported zstd helpers do not validate w. A nil or typed-nil io.Writer can reach zstd.NewWriter in acquireRealZstdWriter, where an error is handled with panic(err), or can be dereferenced by copyZeroAllocWithLimit when writing decompressed data. A caller can trigger a crash by supplying such a writer."
  },
  {
    "severity": "low",
    "title": "Zstd encoder write errors are ignored on fast-path compression",
    "function": "WriteZstdLevel",
    "description": "For *byteSliceWriter, *bytes.Buffer, and *bytebufferpool.ByteBuffer, WriteZstdLevel returns len(p), nil after calling stacklessWriteZstd. nonblockingWriteZstd ignores zw.Write errors, and AppendZstdBytesLevel ignores WriteZstdLevel errors. If the underlying writer or encoder fails, callers may treat a failed or partially written compression as successful."
  }
]
```