diff --git a/coraza-spoa/overrides.conf b/coraza-spoa/overrides.conf index b506f03..f44f7f3 100644 --- a/coraza-spoa/overrides.conf +++ b/coraza-spoa/overrides.conf @@ -47,7 +47,17 @@ SecRuleUpdateActionById 933170-933200 "ctl:ruleEngine=On" # vulnerable, but blocking at the edge keeps logs clean and protects # any future Java workloads. # --------------------------------------------------------------------------- -SecRuleUpdateActionById 944100-944300 "ctl:ruleEngine=On" +# 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 diff --git a/scripts/test-waf-body-inspection.py b/scripts/test-waf-body-inspection.py index 05cfb4d..7b264aa 100644 --- a/scripts/test-waf-body-inspection.py +++ b/scripts/test-waf-body-inspection.py @@ -240,6 +240,28 @@ class BakedPromotions(unittest.TestCase): if re.match(r'SecRuleUpdateActionById\s+(\S*-)?920440\b', l)], '920440 promoted via SecRuleUpdateActionById again') + def test_no_chain_starter_is_promoted(self): + """Chain-starter rules in the promoted ranges (CRS 4.25.0): 920440, + 944110, 944120. None may be covered by a SecRuleUpdateActionById.""" + chain_starters = (920440, 944110, 944120) + promoted = [] + for l in self.lines: + m = re.match(r'SecRuleUpdateActionById\s+(\d+)(?:-(\d+))?\s', l) + if m: + lo = int(m.group(1)) + promoted.append((lo, int(m.group(2) or lo))) + self.assertTrue(promoted, 'no promotions parsed -- the test is blind') + for rid in chain_starters: + self.assertFalse([r for r in promoted if r[0] <= rid <= r[1]], + f'{rid} is a CRS chain starter and must not be promoted') + + def test_log4shell_range_still_promoted(self): + for rid in (944100, 944130, 944150, 944151, 944152, 944300): + self.assertTrue([1 for l in self.lines + if (m := re.match(r'SecRuleUpdateActionById\s+(\d+)(?:-(\d+))?\s', l)) + and int(m.group(1)) <= rid <= int(m.group(2) or m.group(1))], + f'{rid} lost its promotion') + 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],