Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐹
🐹 Go
Not in CISA KEV
MEDIUM severity

Coraza: Truncated multipart body bypasses MULTIPART_STRICT_ERROR (rule 200003) via silent io.ErrUnexpectedEOF handlingGHSA-r3rm-qphw-hh76

MEDIUMFix: corazawaf/coraza@f94c81b

GHSA-r3rm-qphw-hh76 is a medium-severity (CVSS 5.8) CWE-693 vulnerability in github.com/corazawaf/coraza/v3. A fix is available for github.com/corazawaf/coraza/v3 — see the affected versions and patch details below.

Also known asCVE-2026-41508GO-2026-6670
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Oct 23, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

No confirmed exploitation observed yet

  • CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.
  • CISA’s own triage has not observed active exploitation or public proof-of-concept code for this CVE as of its last assessment.

Exploitation and automatability from CISA’s SSVC triage for GHSA-r3rm-qphw-hh76.

EPSS Exploitation Probability

via FIRST.org ↗
0.5%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs41th percentile — riskier than 41% of all scored CVEsHighest risk

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

GHSA-r3rm-qphw-hh76 by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.

Where this sits among everything scored

Of 394,803 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.

Real-World Exposure

1 pkg affected
🐹github.com/corazawaf/coraza/v3

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.

Description

Root Cause

File: internal/bodyprocessors/multipart.go (since commit 3347961b, PR #1453 "feat: ignore unexpected EOF in MIME multipart request body processor", merged 2026-03-06, first shipped in v3.4.0).

The multipart body processor treats io.ErrUnexpectedEOF as a benign condition. Three sites are affected; all mishandle the error the same way.

File branch, filesystem-backed (lines 71–77)

sz, err := io.Copy(temp, p)
if err != nil {
    if !errors.Is(err, io.ErrUnexpectedEOF) {
        v.MultipartStrictError().(*collections.Single).Set("1")
        return err
    }
    seenUnexpectedEOF = true     // <-- flag never set for UnexpectedEOF
}

File branch, TinyGo path (lines 82–88)

sz, err := io.Copy(io.Discard, p)
if err != nil {
    if !errors.Is(err, io.ErrUnexpectedEOF) {
        v.MultipartStrictError().(*collections.Single).Set("1")
        return err
    }
    seenUnexpectedEOF = true     // <-- same gap
}

Field branch (lines 102–113)

data, err := io.ReadAll(p)
if err != nil {
    if !errors.Is(err, io.ErrUnexpectedEOF) {
        v.MultipartStrictError().(*collections.Single).Set("1")
        return err
    }
}
...
if errors.Is(err, io.ErrUnexpectedEOF) {
    break                         // <-- exits loop with no flag set
}

The function then returns nil at line 116 for any body that ended prematurely. Consequences:

  1. MULTIPART_STRICT_ERROR stays at its initial value 0.
  2. REQBODY_ERROR is not propagated either (since ProcessRequest returns nil).
  3. Neither of the two canonical defensive rules shipped in coraza.conf-recommended fires:
    SecRule REQBODY_ERROR "!@eq 0" "id:200002,phase:2,deny,status:400,..."
    SecRule MULTIPART_STRICT_ERROR "!@eq 0" "id:200003,phase:2,deny,status:400,..."
    

All other error branches in the same function (lines 27, 48, 66, 84, 104) correctly set MULTIPART_STRICT_ERROR before returning; this is an inconsistency introduced in #1453, not a systemic issue.

Context — why the error is swallowed

PR #1453 was introduced to support SecRequestBodyLimitAction ProcessPartial: when a body is cut off because it hit the configured request-body limit, the parser should still surface the parts it did receive. The PR legitimately needs to avoid return err on ErrUnexpectedEOF. But it also silenced the strict-error flag, which is the wrong compromise — the flag is exactly how operators observe that something was incomplete. The fix should keep the non-fatal break and still set MULTIPART_STRICT_ERROR, letting the operator decide (via rule 200003 or their own policy) whether partial processing is acceptable.

Impact

Rule 200003 is Coraza's blanket defense against multipart parser-inconsistency evasions — attack classes where the body is crafted so Coraza's Go mime/multipart reader and the backend's multipart parser (PHP, Node, Java, legacy libmodsecurity, etc.) disagree about where fields start or end. The rule fails-closed on any malformed body, so the operator does not need to enumerate every parser-disagreement trick. That defense is now void for any evasion that also truncates the body.

Examples of what becomes reachable:

  • Smuggling a second field past the truncation boundary that the backend's more permissive parser still extracts.
  • Hiding payload bytes after a deliberately malformed Content-Disposition header that the Go parser refuses but the backend accepts.
  • Generic CRS evasion chains that depend on rule 200003 as a catch-all.

The fix is tiny and low-risk. The impact is disproportionately large because rule 200003 is the only defense-in-depth rule for multipart in the recommended config — there is no secondary signal.

Proof of Concept

Start a Coraza-wrapped HTTP server shipping the canonical defensive rules from coraza.conf-recommended:

SecRuleEngine On
SecRequestBodyAccess On
SecRule REQBODY_ERROR "!@eq 0" \
    "id:200002,phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.'"
SecRule MULTIPART_STRICT_ERROR "!@eq 0" \
    "id:200003,phase:2,t:none,log,deny,status:400,msg:'Multipart strict validation failed.'"

Three real-curl requests against the listener:

#BodyExpected (with rule 200003)Observed
1Well-formed field1=benign + closing boundaryHTTP 200HTTP 200 (baseline)
2Two parts, no closing boundary (...MALFORMED_NO_TRAILING_BOUNDARY)HTTP 400HTTP 200
3Mid-part cutoff: name="x"\r\n\r\nabc (no \r\n, no boundary)HTTP 400HTTP 200

Server-side match log is empty for cases 2 and 3: neither rule 200002 nor rule 200003 fires. Example (case 2):

--PoCBoundary12345\r\n
Content-Disposition: form-data; name="field1"\r\n\r\n
benign\r\n
--PoCBoundary12345\r\n
Content-Disposition: form-data; name="truncated"\r\n\r\n
MALFORMED_NO_TRAILING_BOUNDARY

→ HTTP 200, no audit record, transaction.variables.multipartStrictError == 0.

Mitigation

A two-line change per site in internal/bodyprocessors/multipart.go:

if errors.Is(err, io.ErrUnexpectedEOF) {
    v.MultipartStrictError().(*collections.Single).Set("1")
    seenUnexpectedEOF = true   // keep existing break semantics
}

Apply at the three sites (lines 71–77, 82–88, 102–113 in the current code). No change to the return err control flow is needed — the fix only adds the flag-setter alongside the existing seenUnexpectedEOF = true / break path. PR #1453's ProcessPartial goal is preserved.

Operators running in ProcessPartial mode who intentionally allow truncated bodies should pair this with a config-level change (downgrade rule 200003 to detection-only, or scope it with a secondary check on whether SecRequestBodyLimit was actually hit). The engine change above is safe by default — it restores the invariant that malformed multipart always raises MULTIPART_STRICT_ERROR.

Affected versions

Introduced in PR #1453 (commit 3347961b, merged 2026-03-06). First released in v3.4.0 and still present on main at 599ae64a.

Affected: >= 3.4.0, <= 3.7.0.

Releases prior to v3.4.0 returned err on io.ErrUnexpectedEOF and so REQBODY_ERROR would propagate via rule 200002 even if MULTIPART_STRICT_ERROR was not set — a different (arguably stricter) behavior that did not exhibit this bypass.

References

  • internal/bodyprocessors/multipart.go lines 70–113
  • coraza.conf-recommended rule id:200003 (MULTIPART_STRICT_ERROR)
  • PR #1453 — introduction of ErrUnexpectedEOF swallowing
  • coraza.conf-recommended rule id:200002 (REQBODY_ERROR) — also not raised because ProcessRequest returns nil

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐹Gogithub.com/corazawaf/coraza/v3≥ 3.4.0&&< 3.8.03.8.0go get github.com/corazawaf/coraza/v3@v3.8.0

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/corazawaf/coraza/v3, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update github.com/corazawaf/coraza/v3 to 3.8.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-r3rm-qphw-hh76 is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

Frequently Asked Questions

## Root Cause File: `internal/bodyprocessors/multipart.go` (since commit `3347961b`, PR #1453 *"feat: ignore unexpected EOF in MIME multipart request body processor"*, merged 2026-03-06, first shipped in `v3.4.0`). The multipart body processor treats `io.ErrUnexpectedEOF` as a benign condition. Three sites are affected; all mishandle the error the same way. ### File branch, filesystem-backed (lines 71–77) ```go sz, err := io.Copy(temp, p) if err != nil { if !errors.Is(err, io.ErrUnexpectedEOF) { v.MultipartStrictError().(*collections.Single).Set("1") return err }
O3 Security · Impact-Aware SCA

Is GHSA-r3rm-qphw-hh76 in your dependencies?

Find it across Go, including transitive dependencies.

Coraza: Truncated multipart body bypasses…