Files
haproxy-manager-base/coraza-spoa/overrides.conf
T
shadowdaoandClaude Opus 5.5 729098a8f9 fix(edge): CVE-2026-109618/-109619 multipart mitigations (2026.10.6)
PHP "RCE via OOM corruption" (GHSA-w8hw-w9fx-j79w) and the session
upload-progress variant (GHSA-6wmf-4rv5-p3rp), interim until patched PHP is
on every customer image.

HAProxy (hap_listener.tpl, every frontend render, WAF on or off):
  deny 400 multipart/form-data with no Content-Length (HTTP/1.1 chunked,
  HTTP/2-3 streamed bodies) -- vector (a), the post_max_size bypass.
  Measured: Chrome 155 (h2 and h3) and Firefox 153 send content-length on
  form submits, FormData fetch and XHR; only a ReadableStream fetch body
  (duplex:'half') has none.

Coraza (coraza-spoa/overrides.conf, 209610-209616):
  209610       ARGS_NAMES/FILES_NAMES with more than 16 '['
  209611-613   part-header budget (4/part, deny 32 below zero)
  209614-615   more than 64 empty-filename file parts
  209616       multipart PHP_SESSION_UPLOAD_PROGRESS field
Ids sit in the engine range 200000-209999 that WHP's break-glass rule
(991000001) already removes, so no WHP release is needed for break-glass
to lift them. Each flips the engine On for itself, like the day-one
promotions, so they deny in every mode the sidecar runs in.

Limitation: Coraza sees only the first 16 KB of a body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 07:27:42 -07:00

229 lines
14 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# WHP day-one enforce overrides for coraza-spoa.
#
# Global mode in config.yaml is SecRuleEngine DetectionOnly. The rule ID
# ranges below are promoted to enforcement individually, chosen for very
# low false-positive rate on the kinds of customer traffic seen on WHP
# (WordPress, WooCommerce, Divi page builders).
#
# When bumping the upstream coraza-spoa pin (and thus the bundled CRS):
# 1. Skim the CRS CHANGELOG for new/changed rules in these ID ranges.
# 2. Verify they're still high-confidence before promoting the new image.
# 3. Smoke-test in staging detect-only mode for 24h before flipping enforce.
#
# Per-customer false-positive tuning lives in a future per-customer
# override mechanism; v1 is server-wide.
# ---------------------------------------------------------------------------
# 930120 — LFI: explicit traversal to sensitive system files
# (/etc/passwd, /proc/self/, /.ssh/, /etc/shadow, /etc/group, etc.)
# Unambiguous probe pattern; no legitimate site path leads here.
# Note: 930xxx as a whole includes broader traversal patterns that can FP
# on legitimate relative-path file browsers — keep those detect-only.
# ---------------------------------------------------------------------------
SecRuleUpdateActionById 930120 "ctl:ruleEngine=On"
# ---------------------------------------------------------------------------
# 932100-932160 — RCE: Unix shell command injection
# Patterns like `; cat /etc/passwd`, `|whoami`, backtick `\`uname\``,
# $(...) substitution, &&/|| chaining with shell builtins.
# Don't appear in normal POST bodies, URL params, or headers. Targeting
# these is unambiguous attempted command execution.
# ---------------------------------------------------------------------------
SecRuleUpdateActionById 932100-932160 "ctl:ruleEngine=On"
# ---------------------------------------------------------------------------
# 933170-933200 — PHP Webshell access patterns
# Direct requests to known webshell paths: c99.php, r57.php, b374k.php,
# wso.php, alfa.php, mini.php, etc. Almost universally reconnaissance
# scanning for post-exploitation. Even legitimate WordPress installs
# never serve these paths.
# ---------------------------------------------------------------------------
SecRuleUpdateActionById 933170-933200 "ctl:ruleEngine=On"
# ---------------------------------------------------------------------------
# 944100-944300 — Log4Shell / JNDI injection
# `${jndi:ldap://}`, `${jndi:rmi://}`, and obfuscated variants thereof
# in headers, query strings, or bodies. Even our PHP/Node stack isn't
# vulnerable, but blocking at the edge keeps logs clean and protects
# any future Java workloads.
# ---------------------------------------------------------------------------
# 944110 and 944120 are NOT promoted (changed 2026-10-09, 2026.10.2). Both are
# CHAINS whose first link matches bare words anywhere in the request --
# `runtime|processbuilder` and `getproperty|forclosure|filewriter|...` -- and
# Coraza runs an appended ctl:ruleEngine=On on that first link even when the
# chain then fails. Measured (Coraza v3.7.0 + CRS 4.25.0): in detect_only,
# `?q=runtime&id=1' or '1'='1` was 403 while the SQLi alone was 200. Same
# chain-starter defect as 920440. They still add to the anomaly score.
# Log4Shell (944150/944151/944152, ...) stays promoted. WHP mirrors this split
# in coraza_baked_promotion_ranges() (web-files/libs/coraza_blocking_set.php).
SecRuleUpdateActionById 944100 "ctl:ruleEngine=On"
SecRuleUpdateActionById 944130-944300 "ctl:ruleEngine=On"
# ---------------------------------------------------------------------------
# 920440 — URL file extension restricted by policy
# Catches probes for backup / config / dump files: .bak, .old, .save,
# .swp, .sql, .dist, .backup. Promoted to enforce after empirical
# observation on whp01 (2026-05-12, first ~30 min of detect-only):
# 124 events, all backup-file recon — `/wp-config.php.old`,
# `/db_backup.sql`, `/.env.save`, `/releases.sql`, etc. — from a
# single GCP-hosted scanner. Zero false positives observed; standard
# WP/WooCommerce/Divi/HPR URLs do not end in these extensions.
# ---------------------------------------------------------------------------
# HOW IT IS PROMOTED (changed 2026-10-09, haproxy-manager-base 2026.10.2).
# This used to be `SecRuleUpdateActionById 920440 "ctl:ruleEngine=On"`, which
# was WRONG: 920440 is a CHAIN whose first link is
# `REQUEST_BASENAME "@rx \.([^.]+)$"` -- it matches EVERY URL with a file
# extension -- and Coraza runs actions appended to a chain starter when that
# first link matches, even though the chain then fails. Measured (Coraza
# v3.7.0 + CRS 4.25.0, in-process and live): every *.php / *.css / ...
# request ran with the engine On, so the whole CRS anomaly score (949110) and
# 200002 enforced there -- in detect_only mode too, since SecRuleEngine
# DetectionOnly cannot override a runtime ctl.
#
# Now: 920440 itself is untouched (it still sets tx.extension in its first
# link and adds the anomaly score in its second), and this separate phase-1
# rule, which runs after it, flips the engine only when the extension really
# is in tx.restricted_extensions -- the original intent. Removing 920440
# (break-glass, a per-host exception) also removes tx.extension, so this rule
# goes quiet with it. Id 992920440 is outside WHP's generated range
# (990000000-991999999).
SecRule TX:EXTENSION "@within %{tx.restricted_extensions}" \
"id:992920440,phase:1,pass,nolog,t:none,t:lowercase,ctl:ruleEngine=On"
# ---------------------------------------------------------------------------
# 930130 — Restricted File Access Attempt
# Catches dotfile / VCS / config-disclosure probes: .env (and .env.local /
# .env.bak / .env.save variants), .git/config, config.php at root or under
# /admin /backend, etc. Distinct from 930120 (system file paths like
# /etc/passwd); this targets application secret files.
#
# Promoted to enforce on the same observation pass that justified 920440:
# 117 events split across joshuaknapp.net (136), cgdannyb.com (51),
# onlinesupplements.net (23) — all `.env`-class disclosure probes.
# Zero false positives observed. Notably, HPR's `/ccdn.php?filename=...`
# audio delivery path does NOT trigger this rule — verified empirically.
# ---------------------------------------------------------------------------
SecRuleUpdateActionById 930130 "ctl:ruleEngine=On"
# ---------------------------------------------------------------------------
# 209610-209616 -- PHP multipart "RCE via OOM corruption" mitigations
# CVE-2026-109618 (GHSA-w8hw-w9fx-j79w) and CVE-2026-109619
# (GHSA-6wmf-4rv5-p3rp). Interim edge mitigation until patched PHP is on every
# customer image; remove once it is. haproxy-manager-base 2026.10.6.
#
# The advisory's request-body vectors and what covers each:
# (a) multipart with no usable Content-Length (chunked / H2-H3 without
# content-length) streams past post_max_size -> denied 400 by HAProxy
# itself (templates/hap_listener.tpl), before any of this runs.
# (b) unlimited headers per multipart part -> 209611-209613 (budget)
# (c) empty file controls (do not count toward max_file_uploads)
# + deeply nested field names (a[][][]...) -> 209614-209615, 209610
# -619: session upload progress during multipart -> 209616
#
# WHY THESE IDS. 200000-209999 is the Coraza engine range, and WHP's site
# break-glass rule (991000001) removes that WHOLE range for a site in
# emergency mode (coraza_blocking_set_ranges(), WHP_WAF_ENGINE_RANGE_LO..HI in
# web-files/libs/coraza_blocking_set.php). Rules that deny on their own MUST be
# inside the derived blocking set or break-glass leaves them armed; inside the
# engine range they are covered without a WHP release. The engine itself uses
# only 200000-200006 (coraza.conf-recommended); CRS uses none of 2xxxxx. WHP
# also treats the range as staff-only for self-service exceptions.
#
# They switch the engine On for themselves (ctl:ruleEngine=On), exactly like
# the day-one promotions above, so they deny in every mode the sidecar runs in.
#
# LIMITATION, read before trusting them: HAProxy hands Coraza only the first
# buffer of the body (tune.bufsize 16 KB). A request that puts its abusive
# parts after ~16 KB of padding, or whose single part has a >16 KB header
# block, is not seen here. Vector (a) is the one enforced on the full request.
# Coraza's multipart parser (Go mime/multipart) also canonicalises part
# header names and stops at the first part it cannot read.
# ---------------------------------------------------------------------------
# (c) Field-name nesting. Counts '[' in every ARGS / FILES name (query,
# urlencoded and multipart names are all raw, PHP-shaped here; Coraza's JSON
# processor names args json.a.b and never produces '['). WordPress-ecosystem
# form names nest a few levels (menu-item-settings[12][title],
# variable_regular_price[0], input_3[]); PHP's own max_input_nesting_level
# is 64, so 16 is far above real forms and far below PHP's ceiling.
SecRule ARGS_NAMES|FILES_NAMES "@rx (?:[^\[]*\[){17}" \
"id:209610,phase:2,deny,status:403,log,t:none,\
msg:'Parameter name nested more than 16 levels deep (CVE-2026-109618 mitigation)',\
logdata:'%{MATCHED_VAR_NAME}',tag:'whp/cve-2026-109618',ctl:ruleEngine=On"
# (b) Part-header budget. A legitimate part carries 1-2 headers
# (Content-Disposition, usually Content-Type; a few clients add
# Content-Transfer-Encoding). Budget = 4 per part - 1 per part header, and
# the request is denied once it is 32 below zero: a single part needs 37+
# headers, and parts averaging 4 or fewer headers never trip it however many
# there are. Parts are counted by what they BECAME (each part is exactly one
# ARGS_POST or FILES entry), not by Content-Disposition lines, because a part
# can repeat Content-Disposition as easily as any other header.
# @unconditionalMatch runs setvar once PER VALUE (measured), and runs on
# empty values too, so empty fields and empty files are counted.
# No REQBODY_PROCESSOR chain on 209611: a chained link's setvar would run once
# per request, not once per part. On a non-multipart body it only RAISES the
# budget, and MULTIPART_PART_HEADERS (the only thing that lowers it) is empty.
SecRule ARGS_POST|FILES "@unconditionalMatch" \
"id:209611,phase:2,pass,nolog,t:none,setvar:tx.whp_cve109618_hdr_budget=+4"
SecRule MULTIPART_PART_HEADERS "@unconditionalMatch" \
"id:209612,phase:2,pass,nolog,t:none,setvar:tx.whp_cve109618_hdr_budget=-1"
SecRule TX:whp_cve109618_hdr_budget "@lt -32" \
"id:209613,phase:2,deny,status:403,log,t:none,\
msg:'Multipart part(s) with an excessive number of headers (CVE-2026-109618 mitigation)',\
logdata:'budget %{tx.whp_cve109618_hdr_budget}',tag:'whp/cve-2026-109618',ctl:ruleEngine=On"
# (c) Empty file controls: parts with an EMPTY filename (`filename=""` or
# `filename=`), which PHP does not count toward max_file_uploads. Coraza turns
# such a part into an ARGS_POST field (no FILES entry), so they are counted
# from the raw part headers. A browser sends one per untouched <input
# type=file>; legitimate forms have a handful. Denied above 64.
SecRule MULTIPART_PART_HEADERS "@rx (?i)^content-disposition:.*;\s*filename\s*=\s*(?:\"\"|;|$)" \
"id:209614,phase:2,pass,nolog,t:none,setvar:tx.whp_cve109618_empty_files=+1"
SecRule TX:whp_cve109618_empty_files "@gt 64" \
"id:209615,phase:2,deny,status:403,log,t:none,\
msg:'Multipart body with more than 64 empty file parts (CVE-2026-109618 mitigation)',\
logdata:'%{tx.whp_cve109618_empty_files} empty file parts',tag:'whp/cve-2026-109618',ctl:ruleEngine=On"
# -619: session upload progress. The engine flip sits on the LAST chain link
# on purpose: Coraza runs a chain starter's ctl as soon as the first link
# matches, even if the chain then fails (the 920440/944110 defect above).
# PHP only tracks upload progress for a
# multipart field named session.upload_progress.name (default
# PHP_SESSION_UPLOAD_PROGRESS, compared case-sensitively) that precedes the
# file parts, so that is exactly what is denied. WordPress core, WooCommerce,
# Divi, Gravity Forms and CF7 do not use the feature.
SecRule ARGS_POST_NAMES "@streq PHP_SESSION_UPLOAD_PROGRESS" \
"id:209616,phase:2,deny,status:403,log,t:none,chain,\
msg:'Multipart session upload-progress field (CVE-2026-109619 mitigation)',\
tag:'whp/cve-2026-109619'"
SecRule REQBODY_PROCESSOR "@streq MULTIPART" "t:none,ctl:ruleEngine=On"
# ---------------------------------------------------------------------------
# Rule families intentionally kept at DETECT-ONLY for v1 — high FP rate
# on customer mix. Promote individually after observation:
#
# 913xxx (Scanner UAs)— matches legitimate ActivityPub federation
# (Mastodon's "...Bot" UA) and SiteLockSpider (a
# paid customer-security service some sites use).
# Observed on whp01 burn-in 2026-05-13:
# 20/185 hits = ~11% FP rate on HPR + greggfranklin
# + suchascream. Detection adds anomaly score
# either way; enforce upside is low.
# 941xxx (XSS) — Divi rich-text editor saves, TinyMCE submissions
# 942xxx (SQLi) — WP admin queries reflected in params
# 920xxx (other) — most 920xxx rules; 920440 specifically promoted above
# 933150 — PHP injection FP on WooCommerce checkout
# (`session_start` literal appearing in billing form data)
# 950xxx-953xxx — Data leakage / backup-file disclosure (mixed FP)
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# RESERVED RULE-ID RANGE: 990000000 – 990999999
# WHP's coraza_rule_manager generates per-host-exception rules in this range
# (rule ID = 990000000 + target_rule_id). Do NOT add new rules in this range
# from any other source. When bumping the coraza-spoa pin, check the CRS
# changelog for new rules with 9-digit IDs (rare but possible) and re-namespace
# if collision risk emerges.
# ---------------------------------------------------------------------------