Details
## Root Cause
File: `internal/auditlog/formats.go` — multiple sites write attacker-influenced bytes into the Native audit-log stream without escaping `\r` or `\n`:
```go
// 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`.
## 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.
## Proof of Concept
Server with `coraza.conf-recommended`-style defaults:
```conf
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:
```go
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.go` uses `json.Marshal` which escapes `\r\n`).
- Deployments using OCSF output.
- Deployments with `SecAuditEngine Off`.
## References
- `internal/auditlog/formats.go` lines 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` — default `SecAuditLogFormat Native`, `SecAuditLogParts ABIJDEFHZ` / `ABCFHZ` variants
- CWE-117 — Improper Output Neutralization for Logs
- CWE-93 — CRLF Injection