high Patched

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

  1. Run go list -m github.com/corazawaf/coraza/v3 in your project to see the installed version, including copies pulled in by other packages.
  2. 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.
  1. Upgrade github.com/corazawaf/coraza/v3 to 3.8.1 or later, commit the lockfile and redeploy.
  2. 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

Merged from OSV. Last updated 24 Oct 2026. All vulnerabilities · RSS · JSON