You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Large octet-stream request bodies are force-parsed into REQUEST_BODY/ARGS and transformed → memory blow-up (SecRequestBodyNoFilesLimit not enforced; forced URLENCODED vs RAW) #1628
When OWASP CRS is loaded, a large request body with an unknown/binary Content-Type (e.g. application/octet-stream, as used by WebDAV / Nextcloud chunked uploads) is force-loaded into REQUEST_BODYandARGS_POST and then run through CRS transformation chains. This causes a severe memory blow-up and high latency that scales with body size × number of rules, even though the body is a binary file that should not be inspected as form input.
This diverges from ModSecurity v3 (libmodsecurity), which does not url-decode an octet-stream body into ARGS and which enforcesSecRequestBodyNoFilesLimit. The result is that the same CRS config that is safe on ModSecurity exhausts memory on Coraza.
This is the root cause behind corazawaf/coraza-caddy#76 (Caddy + Coraza eating all RAM on Nextcloud sync).
Process RSS: ~56 MB → ~1.9 GB (live heap, captured with pprof -inuse_space, is GC-independent).
Per-chunk latency: ~26 s for a 12 MB body.
Heap breakdown (pprof): ~94% in strings.(*Builder).WriteString / internal/bytealg.MakeNoZero / internal/transformations.doReplaceComments.
The mechanism is per-transaction: each transformation in a rule's chain allocates a fresh full-size copy of the multi-MB value; CRS applies many transformations across many rules, so the cost is O(body_size × rules) in both CPU and short-lived allocations. The strings.Builder allocations come from io.Copy(&strings.Builder, reader) in the urlencoded/raw body processors pulling the entire buffered body (including any disk/tmpfs spill) back into one contiguous string, which transformations then clone repeatedly.
Reproduction
Build Caddy with coraza-caddy and a standard CRS config (recommended conf + crs-setup + @owasp_crs/*.conf), SecRuleEngine On.
PUT a ≥1 MB body with Content-Type: application/octet-stream to any path.
Observe latency and capture a heap profile (:2019/debug/pprof/heap): transformation allocations dominate.
A self-contained docker repro (Caddy + a fake-Nextcloud backend + k6) is available on request.
Root cause
Two independent divergences from ModSecurity; either one mitigates, both should be addressed.
1. Forced URLENCODED processor for unknown content types
CRS rule 901340 issues ctl:forceRequestBodyVariable=On for any non-URLENCODED/MULTIPART/JSON/XML body. In Coraza this is honored in ProcessRequestBody and defaults the processor to URLENCODED for an empty/unknown RBP (internal/corazawaf/transaction.go, ~L1104-1114):
iftx.ForceRequestBodyVariable {
ifrbp=="" {
// TODO(4.x): Evaluate if the new RAW body parser fits better than URLENCODED ...rbp="URLENCODED"
}
tx.variables.reqbodyProcessor.Set(rbp)
}
The URLENCODED processor (internal/bodyprocessors/urlencoded.go) copies the whole body into REQUEST_BODY and ParseQuerys it into ARGS_POST — so a binary body is double-stored and parsed into garbage args, all of which then carry transformations.
In ModSecurity, ctl:forceRequestBodyVariable is a documented no-op (never implemented), and extractArguments("POST", ...) runs only for WWWFormUrlEncoded — octet-stream is never parsed into ARGS.
2. SecRequestBodyNoFilesLimit is parsed but never enforced
The directive is accepted and stored but no logic uses it (internal/corazawaf/waf.go):
// TODO: SecRequestBodyNoFilesLimit directive is retrieving the value, but no logic// based on it is implemented. See https://github.com/corazawaf/coraza/issues/896RequestBodyNoFilesLimitint64
In ModSecurity this limit (recommended 131072) is enforced before the body processor runs (src/transaction.cc), capping the inspected non-file body and skipping parsing when exceeded. Coraza has no such cap, so the full body (default SecRequestBodyLimit is 128 MB in Coraza vs ModSecurity's recommended 13 MB) is parsed and transformed.
Note: multipart file uploads are not the problem — both engines route file part content to a temp file / FILES metadata and never run file bytes through transformations. The issue is specific to the raw octet-stream path that CRS forces into a transformable variable.
Proposed fixes (open to discussion)
Enforce SecRequestBodyNoFilesLimit (Support SecRequestBodyNoFilesLimit directive #896): in ProcessRequestBody, for non-multipart bodies, if the buffered length exceeds the limit, skip the body processor (mirroring ModSecurity). The field already exists.
Default the forced processor to RAW instead of URLENCODED for unknown content types (Raw body processor #938): RAW sets only REQUEST_BODY and avoids the bogus ARGS_POST duplication.
Bound transformations by size, not inspection (preferred safeguard): for body variables above a configurable threshold, skip the transformation chain but still let operators (e.g. @detectSQLi/@detectXSS, which do not require transformations) evaluate the raw value. This bounds memory while preserving content-agnostic detection — strictly better than skipping inspection entirely.
A combination of (2) + (3) keeps the body inspectable (no REQUEST_BODY error, uploads are not blocked) while eliminating the allocation blow-up. (1) is the most faithful to ModSecurity but, depending on CRS policy around REQBODY_ERROR, can affect whether oversized bodies are blocked — so its disruptive behavior should be deliberate/configurable.
Summary
When OWASP CRS is loaded, a large request body with an unknown/binary
Content-Type(e.g.application/octet-stream, as used by WebDAV / Nextcloud chunked uploads) is force-loaded intoREQUEST_BODYandARGS_POSTand then run through CRS transformation chains. This causes a severe memory blow-up and high latency that scales with body size × number of rules, even though the body is a binary file that should not be inspected as form input.This diverges from ModSecurity v3 (libmodsecurity), which does not url-decode an octet-stream body into
ARGSand which enforcesSecRequestBodyNoFilesLimit. The result is that the same CRS config that is safe on ModSecurity exhausts memory on Coraza.This is the root cause behind corazawaf/coraza-caddy#76 (Caddy + Coraza eating all RAM on Nextcloud sync).
Impact / performance
Reproduced with Caddy + coraza-caddy + CRS (
SecRuleEngine On), uploading ~12 MBapplication/octet-streamchunks (Nextcloud chunked-upload pattern), 12 concurrent:pprof -inuse_space, is GC-independent).pprof): ~94% instrings.(*Builder).WriteString/internal/bytealg.MakeNoZero/internal/transformations.doReplaceComments.The mechanism is per-transaction: each transformation in a rule's chain allocates a fresh full-size copy of the multi-MB value; CRS applies many transformations across many rules, so the cost is
O(body_size × rules)in both CPU and short-lived allocations. Thestrings.Builderallocations come fromio.Copy(&strings.Builder, reader)in the urlencoded/raw body processors pulling the entire buffered body (including any disk/tmpfs spill) back into one contiguous string, which transformations then clone repeatedly.Reproduction
@owasp_crs/*.conf),SecRuleEngine On.PUTa ≥1 MB body withContent-Type: application/octet-streamto any path.:2019/debug/pprof/heap): transformation allocations dominate.A self-contained docker repro (Caddy + a fake-Nextcloud backend + k6) is available on request.
Root cause
Two independent divergences from ModSecurity; either one mitigates, both should be addressed.
1. Forced
URLENCODEDprocessor for unknown content typesCRS rule
901340issuesctl:forceRequestBodyVariable=Onfor any non-URLENCODED/MULTIPART/JSON/XML body. In Coraza this is honored inProcessRequestBodyand defaults the processor toURLENCODEDfor an empty/unknown RBP (internal/corazawaf/transaction.go, ~L1104-1114):The
URLENCODEDprocessor (internal/bodyprocessors/urlencoded.go) copies the whole body intoREQUEST_BODYandParseQuerys it intoARGS_POST— so a binary body is double-stored and parsed into garbage args, all of which then carry transformations.In ModSecurity,
ctl:forceRequestBodyVariableis a documented no-op (never implemented), andextractArguments("POST", ...)runs only forWWWFormUrlEncoded— octet-stream is never parsed intoARGS.This is the same concern already noted in #938.
2.
SecRequestBodyNoFilesLimitis parsed but never enforcedThe directive is accepted and stored but no logic uses it (
internal/corazawaf/waf.go):In ModSecurity this limit (recommended
131072) is enforced before the body processor runs (src/transaction.cc), capping the inspected non-file body and skipping parsing when exceeded. Coraza has no such cap, so the full body (defaultSecRequestBodyLimitis 128 MB in Coraza vs ModSecurity's recommended 13 MB) is parsed and transformed.This is #896.
ModSecurity comparison (octet-stream body,
SecRequestBodyAccess On)ctl:forceRequestBodyVariableURLENCODEDREQUEST_BODYARGS_POSTfrom raw bodySecRequestBodyNoFilesLimitNote: multipart file uploads are not the problem — both engines route file part content to a temp file / FILES metadata and never run file bytes through transformations. The issue is specific to the raw octet-stream path that CRS forces into a transformable variable.
Proposed fixes (open to discussion)
SecRequestBodyNoFilesLimit(SupportSecRequestBodyNoFilesLimitdirective #896): inProcessRequestBody, for non-multipart bodies, if the buffered length exceeds the limit, skip the body processor (mirroring ModSecurity). The field already exists.RAWinstead ofURLENCODEDfor unknown content types (Raw body processor #938):RAWsets onlyREQUEST_BODYand avoids the bogusARGS_POSTduplication.@detectSQLi/@detectXSS, which do not require transformations) evaluate the raw value. This bounds memory while preserving content-agnostic detection — strictly better than skipping inspection entirely.A combination of (2) + (3) keeps the body inspectable (no
REQUEST_BODYerror, uploads are not blocked) while eliminating the allocation blow-up. (1) is the most faithful to ModSecurity but, depending on CRS policy aroundREQBODY_ERROR, can affect whether oversized bodies are blocked — so its disruptive behavior should be deliberate/configurable.Related
SecRequestBodyNoFilesLimitdirective #896 —SecRequestBodyNoFilesLimitnot implemented