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 <noreply@anthropic.com>
This commit is contained in:
1 parent
cc0652e0c0
commit
234029c249
2 files changed
+48
-10
No files matched your search
+19
-10
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in new issue
Block a user