Coraza: Silent argument drop at ArgumentLimit allows bypass of ARGS-targeted rules via parameter flooding
CVE-2026-41510 · GHSA-6r3q-mjv7-xr8m · GO-2026-6668 · published 6 Oct 2026
How serious is it
- Severity: high.
- No exploit prediction (EPSS) score yet.
- Not on the CISA list of exploited vulnerabilities.
- No public exploit in Exploit-DB.
Is your app affected by CVE-2026-41510?
Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.
Who is affected
github.com/corazawaf/coraza/v3(Go) >= 3.0.0, < 3.8.1
How to check
- Run
go list -m github.com/corazawaf/coraza/v3in your project to see the installed version, including copies pulled in by other packages. - Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
How to fix
Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
- Upgrade
github.com/corazawaf/coraza/v3to 3.8.1 or later, commit the lockfile and redeploy. - If a vulnerable version was live, check your logs and rotate the secrets that server could read.
The advisory
## Root Cause
File: `internal/corazawaf/transaction.go`, lines 770–808 (since commit 2fd87b89, PR #812, 2023-06-14)
```go
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`:
```go
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`:
```go
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:
```conf
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:
```cpp
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:
```conf
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`) | n…
Check your whole app
Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.
References
- github.com/corazawaf/coraza/security/advisories/GHSA-6r3q-mjv7-xr8m
- github.com/corazawaf/coraza/commit/146c2f79f39ad16f13787e7d67ad400f8d8cb9a3
- github.com/corazawaf/coraza/commit/814e1898e083d2ff2ceb644382d0da17e930f93f
- github.com/corazawaf/coraza/commit/b98359bb7e7606c95100dcc7ad490d2d624e6dea
- github.com/corazawaf/coraza/releases/tag/v3.8.1
- github.com/corazawaf/coraza
Merged from OSV. Last updated 24 Oct 2026. All vulnerabilities · RSS · JSON