Summary
Coraza: Silent argument drop at ArgumentLimit allows bypass of ARGS-targeted rules via parameter flooding
Advisory details
Root Cause
File: internal/corazawaf/transaction.go, lines 770–808 (since commit 2fd87b89, PR #812, 2023-06-14)
func (tx *Transaction) AddGetRequestArgument(key string, value string) {
if tx.checkArgumentLimit(tx.variables.argsGet) {
tx.debugLogger.Warn().Msg("skipping get request argument, over limit")
return
}
tx.variables.argsGet.Add(key, value)
}
func (tx *Transaction) checkArgumentLimit(c *collections.NamedCollection) bool {
return c.Len() >= tx.WAF.ArgumentLimit
}
AddGetRequestArgument, AddPostRequestArgument, and AddPathRequestArgument silently return once the per-collection argument count reaches WAF.ArgumentLimit (default 1000, see internal/corazawaf/waf.go:346). No error variable is set, no transaction flag is raised, and no rule can observe that a drop occurred.
Worse, ExtractGetArguments (transaction.go:761) iterates the map[string][]string returned by urlutil.ParseQuery:
func (tx *Transaction) ExtractGetArguments(uri string) {
data := urlutil.ParseQuery(uri, '&')
for k, vs := range data { // Go map iteration order is randomized
for _, v := range vs {
tx.AddGetRequestArgument(k, v)
}
}
}
Because Go randomizes map iteration order, which of the caller-supplied arguments survive the limit is non-deterministic. An attacker can pad the URI with filler arguments; any one of them — including the malicious payload — may be the one silently discarded, and therefore invisible to every SecRule targeting ARGS, ARGS_GET, or ARGS_NAMES.
Secondary finding — POST urlencoded processor bypasses the cap entirely
internal/bodyprocessors/urlencoded.go:29 populates ARGS_POST without invoking checkArgumentLimit:
values := urlutil.ParseQuery(b, '&')
argsCol := v.ArgsPost()
for k, vs := range values {
argsCol.Set(k, vs) // direct write, no limit check
}
So AddPostRequestArgument's cap is effectively dead code for real urlencoded bodies. ARGS_POST grows unbounded — both a bypass surface and a memory-DoS surface.
Impact
Any SecRule or CRS rule that inspects ARGS, ARGS_GET, ARGS_NAMES, ARGS_GET_NAMES, or ARGS_PATH can be evaded by inflating the request's argument count past SecArgumentsLimit (default 1000). The bypass probability per request scales with overflow:
| Total args in request | Observed bypass rate of ARGS rule |
|---|---|
| 1000 (at limit) | 0 / 50 (0.0%) |
| 1001 (1 over) | 0 / 2000 (< 0.1%) |
| 1100 (100 over) | 45 / 500 (9.0%) |
| 2000 (1000 over) | 106 / 200 (53.0%) |
| 10000 (10× limit) | 47 / 50 (94.0%) |
The rate matches the theoretical model (N − limit) / N. An attacker flooding with 10000 arguments lands a bypass on ~94% of requests; one failed attempt costs them nothing, so a handful of retries yields a near-certain evasion against any ARGS-targeted rule, including the OWASP CRS SQLi, XSS, RCE, and LFI detection families.
The issue is silent — operators see no audit-log entry, no error, and no MULTIPART_STRICT_ERROR-style flag variable, because none exists.
Proof of Concept
Start a Coraza-wrapped HTTP server with a trivial ARGS rule:
SecRuleEngine On
SecRule ARGS "@contains ATTACK_HERE_XYZ" "id:9001,phase:2,deny,status:403,msg:'Attack detected'"
Baseline sanity checks pass:
$ curl -s -o /dev/null -w '%{http_code}\n' 'http://127.0.0.1:8090/?evil=ATTACK_HERE_XYZ'
403
Now pad the URI with 9999 filler parameters and one malicious parameter placed at a random offset. Running 50 such trials against a real HTTP listener with real curl:
=== 10000 args (attacker adds 9999 filler parameters) ===
total_args=10000 trials=50 BLOCKED=3 BYPASS=47 (94.0%)
47 of 50 attack requests were served HTTP 200 despite the payload being present in the URI. The defending rule never fired because Coraza discarded the argument before phase:2 evaluation.
Comparison with ModSecurity v3
The engine-level bug is present in ModSecurity v3 as well — src/transaction.cc:282-291 has the equivalent silent-drop:
bool Transaction::addArgument(...) {
if (m_rules->m_argumentsLimit.m_set
&& m_variableArgs.size() >= m_rules->m_argumentsLimit.m_value) {
ms_dbg(4, "Skipping request argument, over limit (...)")
return false; // return value is ignored at the GET callsite
}
...
}
ModSecurity is in fact more deterministic than Coraza — its query-string parser (extractArguments, transaction.cc:254) splits with an ordered ssplit, so it is the tail of the query that silently drops. An attacker places the payload first and pads the tail; no retry loop needed.
However, ModSecurity's default configuration papers over the engine bug. modsecurity.conf-recommended ships:
SecArgumentsLimit 1000
# If SecArgumentsLimit has been set, you probably want to reject any
# request body that has only been partly parsed. The value used in this
# rule should match what was used with SecArgumentsLimit
SecRule &ARGS "@ge 1000" \
"id:'200007',phase:2,t:none,log,deny,status:400,msg:'Failed to fully parse request body due to large argument count',severity:2"
Because addArgument caps the single m_variableArgs collection at exactly the limit, &ARGS == limit iff the limit was hit — rule 200007 converts silent-drop into explicit HTTP 400. ModSecurity also has a complementary REQBODY_ERROR path: its JSON processor cancels parsing on addArgument failure, and rule 200002 denies on REQBODY_ERROR (verified by test/test-cases/regression/secargumentslimit.json, test 2/2).
Coraza's coraza.conf-recommended ships no equivalent rule. That is what makes the bug exploitable out-of-the-box in Coraza and not in ModSecurity.
| Silent-drop at engine | Compensating default rule | Exploitable out-of-the-box | |
|---|---|---|---|
| ModSecurity v3 | yes | yes (id:200007, &ARGS @ge 1000) |
no — denies at limit |
| Coraza v3 | yes | no | yes |
Mitigation
Recommended fixes, in the order they should be applied. Config-layer (#1) closes the default-install exposure quickly; engine-layer (#2, #3) is the durable fix.
1. Ship compensating rules in coraza.conf-recommended (config-layer, immediate)
Port the ModSecurity guard, but keyed per-collection — Coraza caps ARGS_GET, ARGS_POST, and ARGS_PATH independently, unlike ModSecurity's unified m_variableArgs. A single &ARGS @ge 1000 check on the concatenated collection would false-positive at e.g. GET=500 + POST=500 (no drops occurred but aggregate == 1000):
SecRule &ARGS_GET "@ge 1000" \
"id:200007,phase:2,t:none,log,deny,status:400,msg:'ARGS_GET over SecArgumentsLimit; request partially parsed'"
SecRule &ARGS_POST "@ge 1000" \
"id:200008,phase:2,t:none,log,deny,status:400,msg:'ARGS_POST over SecArgumentsLimit; request partially parsed'"
SecRule &ARGS_PATH "@ge 1000" \
"id:200009,phase:2,t:none,log,deny,status:400,msg:'ARGS_PATH over SecArgumentsLimit; request partially parsed'"
Both &VAR (variable count, internal/seclang/rule_parser.go:40) and @ge (internal/operators/testdata/ge.json) are supported. Thresholds must track SecArgumentsLimit if the operator overrides it.
2. Expose a transaction-visible flag (engine-layer, durable)
Introduce an ARGUMENTS_LIMIT_REACHED collection variable, analogous to MULTIPART_STRICT_ERROR and URLENCODED_ERROR, set to 1 by AddGetRequestArgument / AddPostRequestArgument / AddPathRequestArgument whenever they drop. Replace the config rules above with a single engine-backed check:
SecRule ARGUMENTS_LIMIT_REACHED "@eq 1" \
"id:200006,phase:1,t:none,log,deny,status:413,msg:'Argument limit reached; request rejected'"
This protects operators with hand-rolled configurations, not just those who use the recommended file.
3. Close the POST urlencoded body-processor gap
`internal/bo
References
- https://github.com/advisories/GHSA-6r3q-mjv7-xr8m
- https://github.com/corazawaf/coraza/security/advisories/GHSA-6r3q-mjv7-xr8m
- https://github.com/corazawaf/coraza/commit/146c2f79f39ad16f13787e7d67ad400f8d8cb9a3
- https://github.com/corazawaf/coraza/commit/814e1898e083d2ff2ceb644382d0da17e930f93f
- https://github.com/corazawaf/coraza/commit/b98359bb7e7606c95100dcc7ad490d2d624e6dea
- https://github.com/corazawaf/coraza/releases/tag/v3.8.1
Related vulnerabilities
All Supply chain →- MEDIUMCVE-2026-102682
Pipecat: Public exposure of Pipecat’s development runner can allow unauthenticated bot session creation
- CRITICALCVE-2026-94603
Podman: The `podman run` command can be instructed to disable almost all sandboxing by image annotation
- MEDIUMCVE-2026-100350
kin-openapi deepObject query decoding repeatedly compiles an invariant regular expression
- MEDIUMCVE-2026-59951
MCP Python SDK: Unbounded Session Accumulation in StreamableHTTP Transport Enables Resource Exhaustion
- HIGHCVE-2026-84318
Nginx UI: MCP `nginx_config_add` Bypasses Directive Validation — Authenticated RCE
- HIGHCVE-2026-84317
Nginx UI: MCP nginx_config_add skips config directive validation → authenticated nginx directive injection (RCE on the official image)