From 234029c249312a04fd2f8e984dc2538ecb66fba2 Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Fri, 9 Oct 2026 14:00:22 -0700 Subject: [PATCH] fix(waf): promote 920440 chain-safely, not on its chain starter (round 2) 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 --- coraza-spoa/overrides.conf | 29 +++++++++++++++++++---------- scripts/test-waf-body-inspection.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/coraza-spoa/overrides.conf b/coraza-spoa/overrides.conf index d8a7383..b506f03 100644 --- a/coraza-spoa/overrides.conf +++ b/coraza-spoa/overrides.conf @@ -59,17 +59,26 @@ SecRuleUpdateActionById 944100-944300 "ctl:ruleEngine=On" # single GCP-hosted scanner. Zero false positives observed; standard # WP/WooCommerce/Divi/HPR URLs do not end in these extensions. # --------------------------------------------------------------------------- -# WARNING (measured 2026-10-09, Coraza v3.7.0 + CRS 4.25.0, in-process AND -# live behind haproxy): 920440 is a CHAIN whose first link is +# 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. Coraza runs the appended ctl:ruleEngine=On when that first link -# matches, even though the chain then fails. So on any *.php / *.css / ... -# request the engine is On for the rest of the transaction: 949110 enforces -# the whole CRS anomaly score (a 942100 SQLi GET is 403 on /x.php, 200 on /x) -# and 200002 denies body-parse errors -- in detect_only mode too, because -# SecRuleEngine DetectionOnly cannot override a runtime ctl. Not changed here -# (it would loosen live blocking fleet-wide); see waf-body-hardening-report.md. -SecRuleUpdateActionById 920440 "ctl:ruleEngine=On" +# 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 diff --git a/scripts/test-waf-body-inspection.py b/scripts/test-waf-body-inspection.py index 9e162bb..05cfb4d 100644 --- a/scripts/test-waf-body-inspection.py +++ b/scripts/test-waf-body-inspection.py @@ -217,6 +217,35 @@ class BufsizeCeilings(unittest.TestCase): self.assertTrue(args[0].endswith(' body=req.body')) +CORAZA_OVERRIDES = os.path.join(MODULE_DIR, 'coraza-spoa', 'overrides.conf') + + +@unittest.skipUnless(os.path.exists(CORAZA_OVERRIDES), + 'coraza-spoa/overrides.conf is not shipped in this image; ' + 'this class runs from a repo checkout') +class BakedPromotions(unittest.TestCase): + """920440 is a CHAIN whose first link matches every URL with a file + extension. `SecRuleUpdateActionById 920440 "ctl:ruleEngine=On"` ran on that + first link, so every *.php / *.css request was evaluated with the engine + On (full CRS anomaly enforcement, in detect_only mode too). Measured + in-process and live, 2026-10-09; fixed by keying the flip on the chain's + real condition.""" + + def setUp(self): + with open(CORAZA_OVERRIDES) as fh: + self.lines = active_lines(fh.read()) + + def test_920440_is_not_promoted_on_its_chain_starter(self): + self.assertFalse([l for l in self.lines + if re.match(r'SecRuleUpdateActionById\s+(\S*-)?920440\b', l)], + '920440 promoted via SecRuleUpdateActionById again') + + def test_920440_flip_keys_on_the_restricted_extension(self): + i = self.lines.index('SecRule TX:EXTENSION "@within %{tx.restricted_extensions}" \\') + self.assertEqual(self.lines[i + 1], + '"id:992920440,phase:1,pass,nolog,t:none,t:lowercase,ctl:ruleEngine=On"') + + @unittest.skipUnless(os.path.exists(CORAZA_CONFIG), 'coraza-spoa/config.yaml is not shipped in this image; ' 'this class runs from a repo checkout')