GHSA-PRPW-WWV7-XJJR

Vulnerability from github – Published: 2026-10-06 20:37 – Updated: 2026-10-06 20:37
VLAI
Summary
Coraza: Native audit-log format allows CRLF injection and log forgery via request body and header fields
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:

// 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:

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.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
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 3.7.0"
      },
      "package": {
        "ecosystem": "Go",
        "name": "github.com/corazawaf/coraza/v3"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "3.0.0"
            },
            {
              "fixed": "3.8.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-41504"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-117",
      "CWE-93"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-10-06T20:37:27Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "## Root Cause\n\nFile: `internal/auditlog/formats.go` \u2014 multiple sites write attacker-influenced bytes into the Native audit-log stream without escaping `\\r` or `\\n`:\n\n```go\n// Part B \u2014 request headers (lines 72\u201380)\nfor k, vv := range al.Transaction().Request().Headers() {\n    for _, v := range vv {\n        res.WriteByte(\u0027\\n\u0027)\n        res.WriteString(k)\n        res.WriteString(\": \")\n        res.WriteString(v)   // \u2190 raw\n    }\n}\n\n// Part C \u2014 request body (lines 85\u201386)\nif body := al.Transaction().Request().Body(); body != \"\" {\n    res.WriteString(body)    // \u2190 raw\n    res.WriteByte(\u0027\\n\u0027)\n}\n\n// Part E \u2014 response body (lines 93\u201394)      raw\n// Part F \u2014 response headers (lines 111\u2013118) raw\n// Part H \u2014 error messages (line 125)        raw\n// Part K \u2014 matched-rule raw data (line 151)  raw\n```\n\nThe Native format\u0027s section structure is line-based: sections are delimited by lines of the form `--\u003c10-char-random-prefix\u003e-\u003cPart\u003e--`, 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.\n\nThe 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`.\n\n## Impact\n\nAn attacker who can land bytes into any of the listed audit-log fields can inject arbitrary lines \u2014 including lines that visually resemble new log entries \u2014 into the audit log file of a defender running the default `SecAuditLogFormat Native` configuration. Realistic consequences:\n\n- **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.\n- **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.\n- **Breaking log-parsing tooling.** Grep/awk pipelines, log tailers, and log-rotation tools with line-based assumptions can be poisoned with crafted binary sequences.\n- **Hiding genuine incidents.** An attacker who can also trigger a rule match on the same transaction (trivial \u2014 send any request that matches *any* audit-logged rule) can bury the real match under noise they control.\n\nThe 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.\n\n## Proof of Concept\n\nServer with `coraza.conf-recommended`-style defaults:\n\n```conf\nSecRuleEngine On\nSecAuditEngine On\nSecAuditLogParts ABCFHZ\nSecAuditLogType Serial\nSecAuditLog /tmp/audit.log\nSecAuditLogFormat Native\nSecRequestBodyAccess On\nSecRule REQUEST_METHOD \"@rx .\" \\\n    \"id:1001,phase:1,pass,log,auditlog,msg:\u0027trigger\u0027\"\n```\n\n### Body vector \u2014 reachable via stock `coraza/v3/http` + `net/http`\n\nSend an ordinary urlencoded POST whose body contains raw CRLF sequences and forged boundaries:\n\n```\nPOST / HTTP/1.1\nContent-Type: application/x-www-form-urlencoded\n\nevil=benign\\r\\n--coraza-forged-X--\\r\\nForgedLine: yes\\r\\n--coraza-forged-H--\\r\\n[client \"9.9.9.9\"] FAKE ATTACK ENTRY\n```\n\nResulting audit.log:\n\n```\n--heLNtylvjY-C--\nevil=benign\n--coraza-forged-X--\nForgedLine: yes\n--coraza-forged-H--\n[client \"9.9.9.9\"] FAKE ATTACK ENTRY\n\n--heLNtylvjY-F--\n```\n\nThe 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.\n\n### Header vector \u2014 reachable via non-net/http integrations only\n\nThe same effect applies to Part B (request headers) and Part F (response headers) when a header value contains raw `\\r\\n`. Go\u0027s `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`:\n\n- `coraza-spoa` (HAProxy SPOP agent) forwards headers from HAProxy, which has more permissive validation.\n- `coraza-proxy-wasm` / Envoy WASM hosts forward header values from the upstream proxy.\n- Custom FFI/WASM hosts and any embedder calling `tx.AddRequestHeader(k, v)` with unvalidated bytes.\n\nOther 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.\n\n## Mitigation\n\nEscape `\\r` and `\\n` at every raw-write site in `internal/auditlog/formats.go`. A single package-level helper is sufficient:\n\n```go\nvar logEscaper = strings.NewReplacer(\"\\r\", \"\\\\r\", \"\\n\", \"\\\\n\")\n\n// Part B \u2014 header values:\nres.WriteString(logEscaper.Replace(v))\n\n// Part F \u2014 header values: same\n// Part H \u2014 error messages:\nres.WriteString(logEscaper.Replace(alWithErrMsg.ErrorMessage()))\n// Part K \u2014 matched-rule raw data:\nres.WriteString(logEscaper.Replace(alEntry.Data().Raw()))\n```\n\nFor Part C / Part E (bodies), the choice is policy-dependent:\n\n- **Escape inline** (`logEscaper.Replace(body)`): keeps the log human-readable for text bodies but loses fidelity for binary.\n- **Base64 / hex-encode** the whole part: binary-safe, matches the spirit of ModSecurity v2\u0027s binary-log handling, but less human-readable.\n\nEscaping is the minimum; base64 for bodies is the more conservative default and is a reasonable audit-log-default change.\n\n### Additional defensive measure\n\nConsider lengthening the `boundaryPrefix` random suffix from 10 chars to \u226516 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.\n\n## Affected versions\n\nThe Native formatter has been present since `v3.0.0` (file existed at the first-release commit). All releases `\u003e= 3.0.0, \u003c= 3.7.0` are affected when `SecAuditLogFormat Native` is used with `SecAuditLogType Serial` or `SecAuditLogType Concurrent`.\n\n**Unaffected:**\n\n- Deployments using `SecAuditLogFormat JSON` (`formats_json.go` uses `json.Marshal` which escapes `\\r\\n`).\n- Deployments using OCSF output.\n- Deployments with `SecAuditEngine Off`.\n\n## References\n\n- `internal/auditlog/formats.go` lines 42, 72\u201380 (Part B), 85\u201388 (Part C), 93\u201396 (Part E), 111\u2013118 (Part F), 125 (Part H), 151 (Part K)\n- `coraza.conf-recommended` \u2014 default `SecAuditLogFormat Native`, `SecAuditLogParts ABIJDEFHZ` / `ABCFHZ` variants\n- CWE-117 \u2014 Improper Output Neutralization for Logs\n- CWE-93 \u2014 CRLF Injection",
  "id": "GHSA-prpw-wwv7-xjjr",
  "modified": "2026-10-06T20:37:27Z",
  "published": "2026-10-06T20:37:27Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/corazawaf/coraza/security/advisories/GHSA-prpw-wwv7-xjjr"
    },
    {
      "type": "WEB",
      "url": "https://github.com/corazawaf/coraza/commit/a3079325547c7c1e08522aed4d6468f25d080e25"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/corazawaf/coraza"
    },
    {
      "type": "WEB",
      "url": "https://github.com/corazawaf/coraza/releases/tag/v3.8.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Coraza: Native audit-log format allows CRLF injection and log forgery via request body and header fields"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline

Sightings and recorded dates, grouped by day · UTC

JSON
Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    Related by attack behaviour

    Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.