BudgetScan

Index out of range panic in FormFile on empty file header slice

medium swesweep-fasthttp · server.go · RequestCtx.FormFile · run #4

Description

After checking `fhh == nil`, the code returns `fhh[0]` without verifying `len(fhh) > 0`. If the multipart parser produces a non-nil empty `[]*FileHeader` for a given key (e.g., due to a crafted or edge-case multipart body), this triggers an index-out-of-range panic, crashing the connection goroutine. An attacker can attempt to trigger this by crafting a multipart form where a file field key maps to an empty header list.

Triage

Verdict
INVALID confidence 0.0
Model
qwen/qwen3.8-27b
Rounds
5
Cost
$0.0651 · 232.6 s
The vulnerability report claims that `FormFile` can panic with an index-out-of-range error if `mf.File[key]` returns a non-nil empty slice. To verify this, I must determine if the Go standard library's `mime/multipart` package can produce such a state. The `Form.File` map is populated by `Reader.ReadForm`. In the standard library implementation, the `readForm` method iterates through parts and only assigns to `form.File[fh.Filename]` (or appends) when a valid `FileHeader` is successfully parsed from a part. It uses `append` to add headers to the slice for a key. A slice in a map only becomes non-nil when at least one element is appended to it. The library never initializes a map entry with an empty non-nil slice; if a key is not present, the map lookup returns `nil`. If a key is present, it has at least one element. Therefore, `len(fhh) >= 1` is guaranteed whenever `fhh != nil`. The check `if fhh == nil` in `FormFile` correctly handles the case where the key is missing (returning `nil`), and `fhh[0]` is safe otherwise. Since the bug pattern (non-nil empty slice) cannot be produced by the parser, the vulnerability is not real.

CRUX: The Go standard library's mime/multipart parser does not create non-nil empty slices for keys in the Form.File map; if a key exists, its slice has length >= 1.
GREP: func (r \*Reader) ReadForm
All rounds
[
  {
    "finding_title": "Index out of range panic in FormFile on empty file header slice",
    "verdict": "INVALID",
    "reasoning": "The reported vulnerability claims that `FormFile` panics if the multipart parser returns a non-nil empty `[]*FileHeader` for a given key, because the code checks `if fhh == nil` but then accesses `fhh[0]` without checking `len(fhh) > 0`. I need to verify if the standard library's `mime/multipart` parser can produce such a state. The `FormFile` method in `server.go` is a public API, so it is reachable by attackers via HTTP requests. The key question is whether `mf.File[key]` can be an empty slice. In Go's `mime/multipart` package, the `Reader` struct's internal `m` map (which `Form.File` is populated from) only adds an entry to the map when it successfully parses a `FileHeader`. If a key exists in the map, it means at least one valid file header was parsed for that key. The parser does not create empty slices for keys; it only appends `FileHeader` objects to existing slices or creates new slices with one element. Therefore, `mf.File[key]` cannot be a non-nil empty slice. The check `if fhh == nil` is sufficient to catch the case where the key does not exist in the map (which returns a nil slice for a map lookup on a missing key in Go). If the key exists, `fhh` will have length >= 1. Thus, the panic scenario described (non-nil empty slice) is not possible with the standard `mime/multipart` parser. The code is safe.\n\nCRUX: Whether mime/multipart parser can produce a non-nil empty []string or []*FileHeader value for a key in the File map.\nGREP: func (r \\*Reader) (\\*Form, error)",
    "elapsed": 46.2,
    "tokens": 27692,
    "tokens_in": 27301,
    "tokens_out": 391,
    "cost_usd": 0.01263942,
    "file": "server.go",
    "round": 1,
    "grep_used": true,
    "grep_results": "GREP `Reader`:\n```\nbrotli.go:26:func acquireBrotliReader(r io.Reader) (*brotli.Reader, error) {\nbrotli.go:27:\tv := brotliReaderPool.Get()\nbrotli.go:29:\t\treturn brotli.NewReader(r), nil\nbrotli.go:31:\tzr := v.(*brotli.Reader)\nbrotli.go:38:func releaseBrotliReader(zr *brotli.Reader) {\nbrotli.go:39:\tbrotliReaderPool.Put(zr)\nbrotli.go:42:var brotliReaderPool sync.Pool\nbrotli.go:174:\tr := &byteSliceReader{b: p}\nbrotli.go:175:\tzr, err := acquireBrotliReader(r)\nbrotli.go:180:\treleaseBrotliReader(zr)\nheader_timing_test.go:52:\t\tbr := bufio.NewReader(buf)\nheader_timing_test.go:81:\t\tbr := bufio.NewReader(buf)\nheader_timing_test.go:109:\t\tbr := bufio.NewReader(buf)\nfasthttpadaptor/adaptor.go:209:\tpr *io.PipeReader\nfasthttpadaptor/adaptor.go:328:\treturn conn, &bufio.ReadWriter{Reader: bufio.NewReader(conn), Writer: bufW}, nil\nfasthttpadaptor/request.go:39:\tr.Body = io.NopCloser(bytes.NewReader(body))\nfasthttpadaptor/adaptor_test.go:325:\t\tresp, err := http.ReadResponse(bufio.NewReader(c), nil)\nfasthttpadaptor/adaptor_test.go:411:\t\tresp, err := http.ReadResponse(bufio.NewReader(c), nil)\nfasthttpadaptor/adaptor_test.go:664:\tresp, err := http.ReadResponse(bufio.NewReader(bytes.NewReader(response)), nil)\nstreaming.go:14:\tReadTrailer(r *bufio.Reader) error\nstreaming.go:19:\tprefetchedBytes *bytes.Reader\nstreaming.go:20:\treader          *bufio.Reader\nstreaming.go:89:func acquireRequestStream(b *bytebufferpool.ByteBuffer, r *bufio.Reader, h headerInterface) *requestStream {\nstreaming.go:91:\trs.prefetchedBytes = bytes.NewReader(b.B)\nfs_test.go:32:\tbr := bufio.NewReader(bytes.NewBufferString(s))\nfs_test.go:143:\tbr := bufio.NewReader(bytes.NewBufferString(s))\nfs_test.go:268:\treader, ok := ctx.Response.bodyStream.(*fsSmallFileReader)\nfs_test.go:270:\t\tt.Fatal(\"expected fsSmallFileReader\")\nfs_test.go:318:\tbr := bufio.NewReader(bytes.NewBufferString(s))\nfs_test.go:351:\tbr = bufio.NewReader(bytes.NewBufferString(s))\n```"
  },
  {
    "finding_title": "Index out of range panic in FormFile on empty file header slice",
    "verdict": "INVALID",
    "reasoning": "The reported vulnerability claims that `FormFile` can panic due to an index-out-of-range error if the multipart parser returns a non-nil empty `[]*multipart.FileHeader` slice. To trigger this, an attacker would need to craft a `multipart/form-data` request where a specific file key exists in the `Form.File` map but has an empty value list. \n\nHowever, in Go's standard library `mime/multipart` package, the `Reader.ReadForm` method (which populates the `Form.File` map) iterates through the multipart parts. It only adds a key to the `File` map when it successfully parses a `FileHeader` from a part. The internal logic for building the map (specifically in `readForm` or similar helper functions in the standard library) appends `FileHeader` instances to a slice. A slice is only created and assigned to the map if at least one `FileHeader` is added. There is no code path in the standard library parser that initializes an empty slice for a key in the `File` map. If a key is present in `mf.File`, its corresponding slice has length >= 1. \n\nThe check `if fhh == nil` in `FormFile` handles the case where the key is not present in the map (Go map lookup returns zero value, which is `nil` for a slice). Since the standard library parser never produces a non-nil empty slice for `Form.File` values, `fhh[0]` is always a valid access if `fhh != nil`. Therefore, the bug pattern does not exist in the context of the standard library parser used by fasthttp. The defense is sufficient because the invariant `len(fhh) > 0` whenever `fhh != nil` is maintained by the parser implementation.\n\nCRUX: The standard library mime/multipart parser never creates non-nil empty slices for Form.File entries; it only adds keys when a valid FileHeader is parsed.\nGREP: func (r \\*Reader) readForm",
    "elapsed": 52.6,
    "tokens": 28902,
    "tokens_in": 28465,
    "tokens_out": 437,
    "cost_usd": 0.013266299999999998,
    "file": "server.go",
    "round": 2,
    "grep_used": true,
    "grep_results": "GREP `func (r *Reader) readForm`:\n```\n(no matches in repo)\n```"
  },
  {
    "finding_title": "Index out of range panic in FormFile on empty file header slice",
    "verdict": "INVALID",
    "reasoning": "The vulnerability report claims that `FormFile` can panic if `mf.File[key]` is a non-nil empty slice. This depends on the behavior of Go's standard library `mime/multipart` package, which is used to parse the multipart form. In the standard library implementation, the `Form.File` map is populated by iterating through the multipart parts. A key is added to the `Form.File` map only when a valid `FileHeader` is parsed and successfully appended to the slice for that key. The code does not create empty slices for keys; if a key exists in the map, it means at least one file header was parsed for that key. Therefore, `len(fhh) >= 1` is guaranteed if `fhh` is not nil. The check `if fhh == nil` in `FormFile` correctly handles the case where the key does not exist in the map (which returns a nil slice). Since the standard library parser never produces a non-nil empty slice for `Form.File` values, the `fhh[0]` access is safe. The bug pattern described (non-nil empty slice) cannot be produced by the parser used in fasthttp, so the panic cannot occur via untrusted input.\n\nCRUX: The standard library mime/multipart parser only adds keys to the Form.File map when a valid FileHeader is parsed, ensuring the slice is non-empty.\nGREP: func (r \\*Reader) ReadForm",
    "elapsed": 46.3,
    "tokens": 28672,
    "tokens_in": 28358,
    "tokens_out": 314,
    "cost_usd": 0.012852359999999998,
    "file": "server.go",
    "round": 3,
    "grep_used": true,
    "grep_results": "GREP `func (r *Reader) ReadForm (simplified to: ReadForm)`:\n```\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))\nhttp_test.go:2862:\tform, err := mr.ReadForm(1024)\n```"
  },
  {
    "finding_title": "Index out of range panic in FormFile on empty file header slice",
    "verdict": "INVALID",
    "reasoning": "The vulnerability report claims that `FormFile` can panic because it accesses `fhh[0]` without checking `len(fhh) > 0`, assuming the multipart parser might return a non-nil empty slice. However, `Form.File` is populated by Go's standard library `mime/multipart` package via `ReadForm`. In the standard library implementation, keys are added to the `Form.File` map only when a valid `FileHeader` is successfully parsed and appended to the slice. The map entry is created with a slice containing at least one element. There is no code path in `mime/multipart` that creates an empty, non-nil slice for a key in `Form.File`. If a key does not exist in the map, the map lookup returns `nil` (the zero value for a slice), which is caught by the `if fhh == nil` check in `FormFile`. If a key exists, `len(fhh)` is guaranteed to be at least 1. Therefore, the preconditions for the reported index-out-of-range panic cannot be satisfied through the standard library parser, and the bug is not reachable with untrusted input.\n\nCRUX: The standard library `mime/multipart` parser never produces a non-nil empty slice for `Form.File` entries; it only adds keys when a valid `FileHeader` is parsed, ensuring `len(fhh) >= 1` if `fhh` is not nil.\nGREP: Form.File",
    "elapsed": 27.9,
    "tokens": 29102,
    "tokens_in": 28784,
    "tokens_out": 318,
    "cost_usd": 0.013043279999999999,
    "file": "server.go",
    "round": 4,
    "grep_used": true,
    "grep_results": "GREP `Form.File`:\n```\n(no matches in repo)\n```"
  },
  {
    "finding_title": "Index out of range panic in FormFile on empty file header slice",
    "verdict": "INVALID",
    "reasoning": "The vulnerability report claims that `FormFile` can panic with an index-out-of-range error if `mf.File[key]` returns a non-nil empty slice. To verify this, I must determine if the Go standard library's `mime/multipart` package can produce such a state. The `Form.File` map is populated by `Reader.ReadForm`. In the standard library implementation, the `readForm` method iterates through parts and only assigns to `form.File[fh.Filename]` (or appends) when a valid `FileHeader` is successfully parsed from a part. It uses `append` to add headers to the slice for a key. A slice in a map only becomes non-nil when at least one element is appended to it. The library never initializes a map entry with an empty non-nil slice; if a key is not present, the map lookup returns `nil`. If a key is present, it has at least one element. Therefore, `len(fhh) >= 1` is guaranteed whenever `fhh != nil`. The check `if fhh == nil` in `FormFile` correctly handles the case where the key is missing (returning `nil`), and `fhh[0]` is safe otherwise. Since the bug pattern (non-nil empty slice) cannot be produced by the parser, the vulnerability is not real.\n\nCRUX: The Go standard library's mime/multipart parser does not create non-nil empty slices for keys in the Form.File map; if a key exists, its slice has length >= 1.\nGREP: func (r \\*Reader) ReadForm",
    "elapsed": 59.6,
    "tokens": 29430,
    "tokens_in": 29082,
    "tokens_out": 348,
    "cost_usd": 0.013258439999999998,
    "file": "server.go",
    "round": 5,
    "grep_used": true,
    "grep_results": "GREP `func (r *Reader) ReadForm (simplified to: 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```"
  }
]
Raw scan report for this file

Looking at this file, I need to trace how untrusted data (HTTP requests from `net.Conn`) flows through the server's request-processing loop and identify where malformed input could trigger panics, resource exhaustion, or memory corruption.

**Key untrusted data flows:**
- Raw TCP bytes → `bufio.Reader` → `ctx.Request.Header.Read(br)` → `parseURI()` → `readLimitBody`/`readBodyStream` → `s.Handler(ctx)`
- Multipart form data → `ctx.MultipartForm()` / `ctx.FormValue()` / `ctx.FormFile()`
- Response writing → `writeResponse(ctx, bw)` → `br`/`bw` pools

**Function-by-function analysis:**

1. **`ServeConn` (top-level)**: `handler` parameter is assigned to `s.Handler` without nil check. If nil, `s.Handler(ctx)` in `serveConn` panics. Programmer error but reachable via public API.

2. **`serveConn`**: The main loop. `maxRequestBodySize` defaults to 4MB, bounding body reads. `br.Peek(1)` on keep-alive handles EOF. `s.Handler(ctx)` is called without panic recovery — a panic here kills the goroutine and leaks `br`, `bw`, `ctx` (no deferred cleanup for these). The `concurrency`/`open` counters are properly balanced via `defer s.serveConnCleanup()`.

3. **`FormFile`**: `mf.File[key]` returns `[]*FileHeader`. The nil check catches missing keys, but if the slice is non-nil yet empty (a parser invariant violation), `fhh[0]` panics with index-out-of-range.

4. **`FormValue` / `defaultFormValue`**: Falls through to `ctx.MultipartForm()` → `MultipartFormWithLimit(0)` — no size cap on multipart parsing. Body is bounded by `maxRequestBodySize`, but if the application sets `MaxRequestBodySize` to 0 or very large, an attacker can drive unbounded memory allocation through multipart form parsing.

5. **`TimeoutWithCodeHandler`**: Spawns a goroutine that does `<-concurrencyCh` after `h(ctx)` returns. If `h(ctx)` panics, the goroutine dies and the concurrency slot is never released — a permanent leak per panic.

6. **`acquireByteReader`**: `var b [1]byte; c.Read(b[:])` — bounded, safe. Panic on `n != 1` is a developer sanity check, not reachable with conforming `net.Conn`.

7. **`SaveMultipartFile`**: `fh.Open()` panics on nil `fh`. `path` is used directly in `os.Create`/`os.Rename` — documented as requiring server-trusted paths.

8. **`EarlyHints`**: Reads Link headers from `ctx.Response.Header` (handler-set, not attacker-set). Writes them to the connection. No untrusted data path.

9. **`writeErrorResponse`**: Ignores return values of `writeResponse` and `bw.Flush()` — errors are silently dropped, potentially leaving the connection in an inconsistent state.

10. **Pool type assertions** (`v.(*Server)`, `v.(*RequestCtx)`, `v.(*bufio.Reader)`, `v.(*atomic.Int64)`): Unchecked, but pools are internal and only populated by the server itself. Safe.

```json
[
  {
    "severity": "medium",
    "title": "Index out of range panic in FormFile on empty file header slice",
    "function": "RequestCtx.FormFile",
    "description": "After checking `fhh == nil`, the code returns `fhh[0]` without verifying `len(fhh) > 0`. If the multipart parser produces a non-nil empty `[]*FileHeader` for a given key (e.g., due to a crafted or edge-case multipart body), this triggers an index-out-of-range panic, crashing the connection goroutine. An attacker can attempt to trigger this by crafting a multipart form where a file field key maps to an empty header list."
  },
  {
    "severity": "medium",
    "title": "Unbounded multipart form memory allocation via default FormValue/MultipartForm (DoS)",
    "function": "RequestCtx.FormValue / RequestCtx.MultipartForm",
    "description": "The default `FormValue` implementation and `MultipartForm()` call `MultipartFormWithLimit(0)`, imposing no explicit cap on multipart parsing. While the initial body read is bounded by `MaxRequestBodySize` (4 MiB default), the multipart parser allocates additional memory for file headers, temporary files, and parsed values. If the application sets `MaxRequestBodySize` to 0 or a very large value, an attacker can send a large multipart body and force the server to allocate proportional (or greater) memory, leading to OOM. Even with the default 4 MiB limit, the parser's overhead amplifies per-request memory usage under high concurrency."
  },
  {
    "severity": "low",
    "title": "Concurrency slot leak when handler panics in TimeoutWithCodeHandler",
    "function": "TimeoutWithCodeHandler",
    "description": "The spawned goroutine executes `h(ctx); ch <- struct{}{}; <-concurrencyCh`. If `h(ctx)` panics (e.g., on malformed input reaching the handler), the goroutine terminates before `<-concurrencyCh` executes, permanently leaking one slot in the bounded `concurrencyCh`. Since fasthttp performs no panic recovery, each panicked request permanently reduces available concurrency. Repeated malformed requests can exhaust all concurrency slots, causing all subsequent requests to receive 429 Too Many Responses — a targeted DoS."
  },
  {
    "severity": "low",
    "title": "Nil handler in ServeConn causes panic on first request",
    "function": "ServeConn (package-level)",
    "description": "The exported `ServeConn(c, handler)` assigns `s.Handler = handler` without a nil check. If `handler` is nil, the subsequent `s.Handler(ctx)` call inside `serveConn` invokes a nil function, panicking and crashing the connection goroutine. While this is a programmer error, it is reachable through the public API and results in an unhandled panic with no recovery."
  },
  {
    "severity": "low",
    "title": "Resource leak (br/bw/ctx) on handler panic in serveConn",
    "function": "Server.serveConn",
    "description": "The `serveConn` method acquires `br`, `bw`, and `ctx` from sync.Pools but releases them only at the end of the function body (after the for-loop). If `s.Handler(ctx)` panics, the panic bypasses all cleanup code, leaking the pooled reader, writer, and context. The `defer s.serveConnCleanup()` handles counter bookkeeping but not pool object release. Under repeated panics (e.g., crafted malformed requests), pool objects accumulate, increasing memory pressure until OOM."
  }
]
```