Summary
Coraza: Native audit-log format allows CRLF injection and log forgery via request body and header fields
Root Cause
File: internal/auditlog/formats.go, multiple sites write attacker-influenced bytes into the Native audit-log stream without escaping \r or \n:
// Part B, request headers (lines 72–80)
for k, vv := range al.Transaction().Request().Headers() {
for _, v := range vv {
res.WriteByte('\n')
res.WriteString(k)
res.WriteString(": ")
res.WriteString(v) // ← raw
}
}
// Part C, request body (lines 85–86)
if body := al.Transaction().Request().Body(); body != "" {
res.WriteString(body) // ← raw
res.WriteByte('\n')
}
// Part E, response body (lines 93–94) raw
// Part F, response headers (lines 111–118) raw
// Part H, error messages (line 125) raw
// Part K, matched-rule raw data (line 151) raw
The Native format's section structure is line-based: sections are delimited by lines of the form --<10-char-random-prefix>-<Part>--, and line-based log parsers / SIEM rules rely on that structure. Any attacker-controlled bytes containing \n break the structural invariant and allow the attacker to inject lines that look like genuine audit content.
The other two Native-format implementations in Coraza are not affected: the JSON formatter (formats_json.go) and the OCSF formatter both round-trip values through json.Marshal, which escapes \r and \n.
Proof of Concept
Server with coraza.conf-recommended-style defaults:
SecRuleEngine On
SecAuditEngine On
SecAuditLogParts ABCFHZ
SecAuditLogType Serial
SecAuditLog /tmp/audit.log
SecAuditLogFormat Native
SecRequestBodyAccess On
SecRule REQUEST_METHOD "@rx ." \
"id:1001,phase:1,pass,log,auditlog,msg:'trigger'"
Body vector, reachable via stock coraza/v3/http + net/http
Send an ordinary urlencoded POST whose body contains raw CRLF sequences and forged boundaries:
POST / HTTP/1.1
Content-Type: application/x-www-form-urlencoded
evil=benign\r\n--coraza-forged-X--\r\nForgedLine: yes\r\n--coraza-forged-H--\r\n[client "9.9.9.9"] FAKE ATTACK ENTRY
Resulting audit.log:
--heLNtylvjY-C--
evil=benign
--coraza-forged-X--
ForgedLine: yes
--coraza-forged-H--
[client "9.9.9.9"] FAKE ATTACK ENTRY
--heLNtylvjY-F--
The forged --coraza-forged-X-- / --coraza-forged-H-- boundaries and the spoofed [client "9.9.9.9"] line are structurally indistinguishable from the surrounding genuine log content. No rule fires, no error is raised, the attack is invisible to the WAF.
Header vector, reachable via non-net/http integrations only
The same effect applies to Part B (request headers) and Part F (response headers) when a header value contains raw \r\n. Go's net/http rejects such headers at parse time (400 Bad Request), so the stock HTTP wrapper is safe from this path; the vector is reachable when Coraza is called with header values that were not validated by net/http:
coraza-spoa(HAProxy SPOP agent) forwards headers from HAProxy, which has more permissive validation.coraza-proxy-wasm/ Envoy WASM hosts forward header values from the upstream proxy.- Custom FFI/WASM hosts and any embedder calling
tx.AddRequestHeader(k, v)with unvalidated bytes.
Other raw-write sites (Part E response body, Part H error messages, Part K matched-rule data) share the same class of issue and should be fixed together.
Mitigation
Escape \r and \n at every raw-write site in internal/auditlog/formats.go. A single package-level helper is sufficient:
var logEscaper = strings.NewReplacer("\r", "\\r", "\n", "\\n")
// Part B, header values:
res.WriteString(logEscaper.Replace(v))
// Part F, header values: same
// Part H, error messages:
res.WriteString(logEscaper.Replace(alWithErrMsg.ErrorMessage()))
// Part K, matched-rule raw data:
res.WriteString(logEscaper.Replace(alEntry.Data().Raw()))
For Part C / Part E (bodies), the choice is policy-dependent:
- Escape inline (
logEscaper.Replace(body)): keeps the log human-readable for text bodies but loses fidelity for binary. - Base64 / hex-encode the whole part: binary-safe, matches the spirit of ModSecurity v2's binary-log handling, but less human-readable.
Escaping is the minimum; base64 for bodies is the more conservative default and is a reasonable audit-log-default change.
Additional defensive measure
Consider lengthening the boundaryPrefix random suffix from 10 chars to ≥16 chars (line 42). This strictly raises the bar for attackers attempting to fully forge a separate-looking session (not just inject lines into the current one). Low-cost change; narrows future variants of this bug class.
Affected versions
The Native formatter has been present since v3.0.0 (file existed at the first-release commit). All releases >= 3.0.0, <= 3.7.0 are affected when SecAuditLogFormat Native is used with SecAuditLogType Serial or SecAuditLogType Concurrent.
Unaffected:
- Deployments using
SecAuditLogFormat JSON(formats_json.gousesjson.Marshalwhich escapes\r\n). - Deployments using OCSF output.
- Deployments with
SecAuditEngine Off.
References
internal/auditlog/formats.golines 42, 72–80 (Part B), 85–88 (Part C), 93–96 (Part E), 111–118 (Part F), 125 (Part H), 151 (Part K)coraza.conf-recommended, defaultSecAuditLogFormat Native,SecAuditLogParts ABIJDEFHZ/ABCFHZvariants- CWE-117, Improper Output Neutralization for Logs
- CWE-93, CRLF Injection
Impact
An attacker who can land bytes into any of the listed audit-log fields can inject arbitrary lines, including lines that visually resemble new log entries, into the audit log file of a defender running the default SecAuditLogFormat Native configuration. Realistic consequences:
- Forging entries to shift attribution. An injected line such as
[client "9.9.9.9"] Coraza: Warning. ...sits alongside genuine matches in Part H, and a human operator (or simple SIEM rule) reading the log cannot tell them apart. - Confusing SIEM correlation. Any ingestion pipeline that splits on
--...-[A-Z]--boundaries or on[client "..."]patterns without validating the session prefix will treat the forged lines as separate records. - Breaking log-parsing tooling. Grep/awk pipelines, log tailers, and log-rotation tools with line-based assumptions can be poisoned with crafted binary sequences.
- Hiding genuine incidents. An attacker who can also trigger a rule match on the same transaction (trivial, send any request that matches any audit-logged rule) can bury the real match under noise they control.
The forged lines cannot trivially impersonate an entire separate session: the 10-char random prefix in the real boundaries (boundaryPrefix := "--" + utils.RandomString(10) + "-", line 42) is not predictable from outside, and each transaction uses a fresh prefix. But the integrity of a single record is fully compromised, which is enough for the SIEM-confusion and attribution-shifting attacks.
CVE-2026-41504 has a CVSS score of 5.8 (Medium). The vector is network-reachable, no privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (3.8.0); upgrading removes the vulnerable code path.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Already deployed Kodem?
See it in your environmentNew to Kodem? Get a demo →Remediation advice
Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.
Frequently Asked Questions
- What is CVE-2026-41504? CVE-2026-41504 is a medium-severity security vulnerability in github.com/corazawaf/coraza/v3 (go), affecting versions >= 3.0.0, <= 3.7.0. It is fixed in 3.8.0.
- How severe is CVE-2026-41504? CVE-2026-41504 has a CVSS score of 5.8 (Medium). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
- Which versions of github.com/corazawaf/coraza/v3 are affected by CVE-2026-41504? github.com/corazawaf/coraza/v3 (go) versions >= 3.0.0, <= 3.7.0 is affected.
- Is there a fix for CVE-2026-41504? Yes. CVE-2026-41504 is fixed in 3.8.0. Upgrade to this version or later.
- Is CVE-2026-41504 exploitable, and should I be worried? Whether CVE-2026-41504 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether CVE-2026-41504 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix CVE-2026-41504? Upgrade
github.com/corazawaf/coraza/v3to 3.8.0 or later.