- 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>
104 lines
4.7 KiB
YAML
104 lines
4.7 KiB
YAML
# Coraza-SPOA configuration for WHP haproxy-manager integration.
|
|
#
|
|
# One named application "haproxy" — the haproxy-manager spoe template
|
|
# references this same name in its spoe-agent block, so the SPOA knows
|
|
# which rules to apply when HAProxy dispatches a request.
|
|
#
|
|
# Mode: SecRuleEngine DetectionOnly globally; overrides.conf promotes
|
|
# specific high-confidence rule ID ranges to enforcement individually.
|
|
# This is the safest posture for v1 — every rule logs, but only the
|
|
# unambiguous ones (scanner UAs, RCE, LFI, webshells, Log4Shell) block.
|
|
|
|
bind: 0.0.0.0:9000
|
|
|
|
# Process-level logging (separate from per-request audit logging below)
|
|
log_level: info
|
|
log_file: /dev/stdout
|
|
log_format: json
|
|
|
|
# Fallback when the request doesn't match a named application — we only
|
|
# have one, so it's also the default.
|
|
default_application: haproxy
|
|
|
|
applications:
|
|
- name: haproxy
|
|
directives: |
|
|
# CRS-bundled defaults: recommended Coraza settings + CRS setup +
|
|
# the rule pack itself (~16 MB of rules embedded in the binary).
|
|
Include @coraza.conf-recommended
|
|
|
|
# Request-body limits, set explicitly instead of inheriting the
|
|
# recommended file's 12.5 MB / Reject. HAProxy only ever hands us one
|
|
# buffer of body (tune.bufsize 16384 in haproxy-manager's
|
|
# hap_header.tpl), and the SPOP library refuses frames over 65535
|
|
# bytes, so no body we receive can exceed 64 KB. The invariant is:
|
|
# tune.bufsize (16 KB) < SPOP frame cap (65535) < SecRequestBodyLimit
|
|
# Breaking it is NOT harmless: SecRequestBodyLimitAction Reject
|
|
# interrupts with 413 even under DetectionOnly (Coraza
|
|
# setAndReturnBodyLimitInterruption ignores the engine mode), which the
|
|
# edge turns into a 403 on every large upload. ProcessPartial is the
|
|
# belt to that brace. InMemoryLimit == BodyLimit keeps the body buffer
|
|
# off disk (this image is distroless). Pinned by
|
|
# haproxy-manager-base/scripts/test-waf-body-inspection.py.
|
|
SecRequestBodyLimit 131072
|
|
SecRequestBodyInMemoryLimit 131072
|
|
SecRequestBodyLimitAction ProcessPartial
|
|
|
|
# JSON nesting limit (the recommended file sets 1024). Coraza's JSON
|
|
# flattener is quadratic in nesting: a 601-byte `[[[...]]]` of depth 300
|
|
# cost 297 ms and depth 1000 (2 KB) 3.1 s of CPU -- past the 100 ms SPOE
|
|
# timeout, so the request passed UNINSPECTED (fail-open) while the WAF
|
|
# burned CPU. Over the limit the body fails to parse (REQBODY_ERROR,
|
|
# 0.1 ms), which WHP's enforce-mode body-integrity rule denies on a
|
|
# complete body. Legitimate depth, measured on the JSON shipped in
|
|
# WordPress 6.x core, Gutenberg, WooCommerce and Elementor (theme.json,
|
|
# styles/*.json, block.json, checkout schemas): max 8, so a global-styles
|
|
# save is ~9; 64 leaves ~7x headroom for Divi/Elementor-style module
|
|
# trees that could not be sampled. Depth 64 itself costs ~21 ms.
|
|
SecRequestBodyJsonDepthLimit 64
|
|
|
|
Include @crs-setup.conf.example
|
|
|
|
# Runtime-managed PRE-CRS exclusions written by WHP UI. Empty by default.
|
|
# Loaded BEFORE the CRS rules so per-host ctl:ruleRemoveById exemptions
|
|
# fire in phase:1 BEFORE the CRS rule they're trying to exempt would
|
|
# otherwise match. Server-wide overrides live in local-overrides.conf
|
|
# (loaded after CRS) instead.
|
|
Include /etc/coraza/pre-overrides.conf
|
|
|
|
Include @owasp_crs/*.conf
|
|
|
|
# WHP-specific overrides — day-one enforce list, plus tuning for
|
|
# the customer mix (WordPress, WooCommerce, Divi). Read this file
|
|
# to see exactly what blocks vs what's detect-only.
|
|
Include /etc/coraza/overrides.conf
|
|
|
|
# Runtime-managed POST-CRS overrides written by WHP UI. Empty by default.
|
|
Include /etc/coraza/local-overrides.conf
|
|
|
|
# Global mode: log all alerts, block only what overrides.conf
|
|
# explicitly promotes via ctl:ruleEngine=On.
|
|
SecRuleEngine DetectionOnly
|
|
|
|
# Audit log: JSON to a bind-mounted file so AI Monitor + log
|
|
# rotation can pick it up. RelevantOnly means we don't log every
|
|
# passing request, only ones that triggered at least one rule.
|
|
SecAuditEngine RelevantOnly
|
|
SecAuditLog /var/log/coraza/audit.log
|
|
SecAuditLogFormat JSON
|
|
SecAuditLogParts ABIJDEFHKZ
|
|
|
|
# HAProxy sends request-only events for v1. Response inspection adds
|
|
# latency on every page render with marginal additional protection
|
|
# for our customer mix; can be turned on later if we want it.
|
|
response_check: false
|
|
|
|
# Transactions cache for 60s. SPOE protocol is fire-and-forget per
|
|
# request, so this is just how long Coraza holds context for any
|
|
# multi-stage processing.
|
|
transaction_ttl_ms: 60000
|
|
|
|
log_level: info
|
|
log_file: /var/log/coraza/spoa.log
|
|
log_format: json
|