CVE-2026-41508Medium· 5.8▾ SunlitCoraza: Truncated multipart body bypasses MULTIPART_STRICT_ERROR (rule 200003) via silent io.ErrUnexpectedEOF handling
▾ Sunlit zone — Low / medium · no exploitation signal
impact 31.9 · likelihood 0 · exploitation 0
Need a working PoC? Pro members can cast a request and our team develops one — it lands right here.
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.
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
}
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
}
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:
MULTIPART_STRICT_ERROR stays at its initial value 0.REQBODY_ERROR is not propagated either (since ProcessRequest returns nil).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.
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.
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:
Content-Disposition header that the Go parser refuses but the backend accepts.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.
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:
| # | Body | Expected (with rule 200003) | Observed |
|---|---|---|---|
| 1 | Well-formed field1=benign + closing boundary | HTTP 200 | HTTP 200 (baseline) |
| 2 | Two parts, no closing boundary (...MALFORMED_NO_TRAILING_BOUNDARY) | HTTP 400 | HTTP 200 |
| 3 | Mid-part cutoff: name="x"\r\n\r\nabc (no \r\n, no boundary) | HTTP 400 | HTTP 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.
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.
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.
internal/bodyprocessors/multipart.go lines 70–113coraza.conf-recommended rule id:200003 (MULTIPART_STRICT_ERROR)ErrUnexpectedEOF swallowingcoraza.conf-recommended rule id:200002 (REQBODY_ERROR) — also not raised because ProcessRequest returns nilgithub.com/corazawaf/coraza/v3 >= 3.4.0, <= 3.7.0Upgrade to a patched release:
github.com/corazawaf/coraza/v3 3.8.0Connected by shared product, vendor, weakness, or advisory.
CVE-2026-41504Medium· 5.8Coraza: Native audit-log format allows CRLF injection and log forgery via request body and header fields
CVE-2026-41510High· 7.2Coraza: Silent argument drop at ArgumentLimit allows bypass of ARGS-targeted rules via parameter flooding
CVE-2025-48612High· 7.8In setDefaultKey of DefaultPaymentSettings.java, there is a possible way for an application to set the main user's default NFC payment setting due to improper input validation
CVE-2025-48601Medium· 5.5In multiple locations, there is a possible permanent denial of service due to improper input validation
CVE-2020-5403High· 7.5Reactor Netty HttpServer, versions 0.9.3 and 0.9.4, is exposed to a URISyntaxException that causes the connection to be closed prematurely instead of producing a 400 response.
CVE-2021-45105Medium· 5.9Apache Log4j2 versions 2.0-alpha1 through 2.16.0 (excluding 2.12.3 and 2.3.1) did not protect from uncontrolled recursion from self-referential lookups