BudgetScan

Abandoned reader can leave stream writer blocked indefinitely

low swesweep-fasthttp · stream.go · NewStreamReader · run #4

Description

The returned reader must be consumed and Close()d, but the API provides no timeout or cancellation. If a client stops reading or the caller abandons the reader, the writer goroutine can block indefinitely in bufio/pipe backpressure. An attacker can open many connections and not read response bodies to accumulate blocked goroutines and pipe objects, causing resource exhaustion. Severity is low because the documented contract requires Close, but the primitive is unsafe in hostile HTTP environments.

Triage

Not triaged.

Raw scan report for this file

## Security analysis

The exported entry point is `NewStreamReader`. It accepts a caller-supplied `StreamWriter` callback, creates an in-memory pipe pair, wraps the write end in a `bufio.Writer`, starts a goroutine to invoke the callback, and returns the read end.

Relevant data flow:

```text
attacker-derived data
  -> host handler / sw closure
  -> bw.Write(...)
  -> bufio buffer
  -> bw.Flush()
  -> pw PipeConn
  -> pr PipeConn
  -> HTTP response body written to client
```

Because this is Go code, there are no C-style stack/heap overflows from the `bufio` or pipe buffers. The main risks are:

1. **Goroutine panic isolation / nil callback**
   - `sw` is a function value and may be `nil`.
   - The goroutine calls `sw(bw)` without checking it.
   - There is no `recover()` in the goroutine.
   - If `sw` panics while processing untrusted input, the panic occurs outside the normal request-handler recovery path and can crash the host process.
   - If the process is somehow kept alive by an external recovery mechanism, the panic path also skips `pw.Close()` and `streamWriterBufPool.Put(bw)`, leaking pipe and buffer resources.

2. **Reader lifecycle / backpressure**
   - The returned reader `pr` must be read and `Close`d.
   - If a client stops reading, or the caller abandons `pr`, the writer goroutine can block indefinitely once the pipe/bufio buffers fill.
   - There is no timeout, cancellation, or way to abort the stream writer.
   - An attacker can potentially open many connections and stop reading response bodies to accumulate blocked goroutines and pipe objects.

3. **Non-findings / lower-priority issues**
   - `v.(*bufio.Writer)` could panic if the pool were poisoned, but the pool is package-private and only this code puts `*bufio.Writer` values into it. This is not realistically attacker-controlled.
   - Ignored errors from `bw.Flush()` and `pw.Close()` are not directly exploitable here; they may mask errors, but they do not create memory corruption or obvious remote DoS beyond the existing lifecycle issues.
   - Unbounded streamed input is a property of the streaming API. The file itself does not enforce length limits, but that is usually a host/handler policy issue.

```json
[
  {
    "severity": "high",
    "title": "Stream writer goroutine lacks panic recovery and cleanup",
    "function": "NewStreamReader",
    "description": "The goroutine created by NewStreamReader calls the caller-supplied StreamWriter without recover and without guaranteed cleanup. If sw panics while processing attacker-controlled request data, the panic occurs outside the normal request-handler recovery path and can terminate the host process, causing denial of service. On the panic path pw is not closed and bw is not returned to streamWriterBufPool, so any recovery harness would also leak pipe and buffered-writer resources."
  },
  {
    "severity": "medium",
    "title": "Nil StreamWriter accepted without validation",
    "function": "NewStreamReader",
    "description": "NewStreamReader accepts a nil StreamWriter. The goroutine invokes sw(bw) without checking for nil, causing a nil function-call panic. If host code selects a nil writer based on an attacker-controlled path, header, or parameter, this is a remotely triggerable denial-of-service condition."
  },
  {
    "severity": "low",
    "title": "Abandoned reader can leave stream writer blocked indefinitely",
    "function": "NewStreamReader",
    "description": "The returned reader must be consumed and Close()d, but the API provides no timeout or cancellation. If a client stops reading or the caller abandons the reader, the writer goroutine can block indefinitely in bufio/pipe backpressure. An attacker can open many connections and not read response bodies to accumulate blocked goroutines and pipe objects, causing resource exhaustion. Severity is low because the documented contract requires Close, but the primitive is unsafe in hostile HTTP environments."
  }
]
```