Owner ruling (same mode contract as 920440: no accidental enforcement from
bare words). 944110 and 944120 are CRS chains whose first link matches
`runtime|processbuilder` / `getproperty|forclosure|filewriter|...`
anywhere in the request; the range promotion 944100-944300 put
ctl:ruleEngine=On on those starters, so any request containing the word
ran with full anomaly enforcement, in detect_only too.
Promotion is now 944100 and 944130-944300. Live (haproxy-manager-base
:wafbody-r3 + coraza-spoa :wafbody-r3), both modes: `?q=runtime&<SQLi>`
and `?q=getproperty&<SQLi>` 403 -> 200 on detect_only; Log4Shell in the
query (plain and ${lower:j} obfuscated), User-Agent and a JSON body still
403.
test-waf-body-inspection.py: 21 tests (+2: no CRS chain starter
920440/944110/944120 is covered by a promotion; the Log4Shell ids keep
theirs). Red on round 2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner decision. `SecRuleUpdateActionById 920440 "ctl:ruleEngine=On"` ran on
920440's chain STARTER (`REQUEST_BASENAME "@rx \.([^.]+)$"`), which matches
every URL with a file extension, so every *.php/*.css request was evaluated
with the engine On: full CRS anomaly enforcement (949110) and 200002, in
detect_only mode too.
Replaced by `SecRule TX:EXTENSION "@within %{tx.restricted_extensions}"
... ctl:ruleEngine=On` (id 992920440, phase 1, after 920440). CRS 4.25.0
sets tx.extension in 920440's first link (REQUEST-920...conf:667, the
coreruleset version coraza-spoa v0.7.1 pins). Live, both modes:
.bak/.sql/.save/.old/.swp probes still 403; benign .php/.css 200; a
942100 SQLi on /x.php and the onclick / "select ... union" admin saves are
no longer 403 in detect_only.
test-waf-body-inspection.py: 19 tests (2 new, repo-checkout only).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- hap_listener.tpl (Coraza block): `http-request deny deny_status 400 if
{ req.fhdr_cnt(content-type) gt 1 }`. With two Content-Type lines Coraza
evaluates every value (text/xml on either line selects its XML processor,
which accepts a form body without error) while Apache merges the lines
and PHP parses the body as a form. Measured with Coraza v3.7.0 + PHP 8.3:
`application/x-www-form-urlencoded` + `text/xml` put the payload in
$_POST uninspected. Live: two lines -> 400 over HTTP/1.1 and HTTP/2; one
comma-joined line (browser XHR double-set) is unaffected.
- coraza-spoa/config.yaml: SecRequestBodyJsonDepthLimit 64 (was 1024 from
the recommended file). Coraza's JSON flattening is quadratic in depth:
depth 300 (601 B) = 297 ms, depth 1000 (2 KB) = 3.1 s, past the 100 ms
fail-open timeout. Over the limit it is a 0.1 ms parse error, which WHP's
enforce-mode rule denies on a complete body (live: depth 65/300/1000 ->
403, depth 64 and a depth-9 global-styles body -> 200). Legitimate depth
measured on WordPress core, Gutenberg, WooCommerce and Elementor JSON:
max 8.
- test-waf-body-inspection.py: 17 tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measured on haproxy 3.0.11 + coraza-spoa v0.7.1 + CRS 4.25.0 (local
haproxy-manager + coraza-spoa + echo backend):
- send-spoe-group fired at header time, so req.body held only what arrived
with the headers. A 933180 payload whose body came in a second send(),
2s late, or after Expect: 100-continue passed uninspected; 8 parallel
14 KB payload-first POSTs: 6/40 blocked before, 40/40 after.
- Add `option http-buffer-request` (Coraza block only). Uploads are not
held: one buffer is inspected, the rest streams (50 MB upload: sha-ok,
same timing before/after).
- Stamp X-WHP-WAF-Body: complete|partial for Coraza (client copies
stripped first, stripped again before the backend). Content-Length:
exact compare; chunked/no-CL: body+headers < 12 KB => complete. WHP's
generated rules use it to deny body-parse errors (200002/200003) only on
complete bodies.
- Pin tune.bufsize 16384 (= default) when Coraza is on. Coraza costs
~3.3 ms/KB of body and the 100 ms SPOE timeout fails open: at 32 KB a
payload at byte 0 of a 30 KB base64 body bypassed. SPOP frames > 65535
are refused by the agent library.
- coraza-spoa/config.yaml: SecRequestBodyLimit 131072 / InMemory 131072 /
ProcessPartial (Reject 413s even under DetectionOnly).
- Access log gains wafb= (complete|partial) and wafe= (SPOE error =
request passed uninspected).
- overrides.conf: document (comment only) that the 920440 promotion turns
the engine On for every URL with a file extension.
- scripts/test-waf-body-inspection.py (16 tests), wired into the build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>