fix(waf): stop promoting 944110/944120 chain starters (round 3)

Owner ruling (same mode contract as 920440: no accidental enforcement from
bare words). 944110 and 944120 are CRS chains whose first link matches
`runtime|processbuilder` / `getproperty|forclosure|filewriter|...`
anywhere in the request; the range promotion 944100-944300 put
ctl:ruleEngine=On on those starters, so any request containing the word
ran with full anomaly enforcement, in detect_only too.

Promotion is now 944100 and 944130-944300. Live (haproxy-manager-base
:wafbody-r3 + coraza-spoa :wafbody-r3), both modes: `?q=runtime&<SQLi>`
and `?q=getproperty&<SQLi>` 403 -> 200 on detect_only; Log4Shell in the
query (plain and ${lower:j} obfuscated), User-Agent and a JSON body still
403.

test-waf-body-inspection.py: 21 tests (+2: no CRS chain starter
920440/944110/944120 is covered by a promotion; the Log4Shell ids keep
theirs). Red on round 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shadowdaoandClaude Opus 5.5 committed 2026-10-09 14:10:41 -07:00
1 parent 234029c249
commit 2da2020305
2 files changed
+33 -1

No files matched your search

+11 -1
View File
@@ -47,7 +47,17 @@ SecRuleUpdateActionById 933170-933200 "ctl:ruleEngine=On"
# vulnerable, but blocking at the edge keeps logs clean and protects # vulnerable, but blocking at the edge keeps logs clean and protects
# any future Java workloads. # 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 # 920440 — URL file extension restricted by policy
+22
View File
@@ -240,6 +240,28 @@ class BakedPromotions(unittest.TestCase):
if re.match(r'SecRuleUpdateActionById\s+(\S*-)?920440\b', l)], if re.match(r'SecRuleUpdateActionById\s+(\S*-)?920440\b', l)],
'920440 promoted via SecRuleUpdateActionById again') '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): def test_920440_flip_keys_on_the_restricted_extension(self):
i = self.lines.index('SecRule TX:EXTENSION "@within %{tx.restricted_extensions}" \\') i = self.lines.index('SecRule TX:EXTENSION "@within %{tx.restricted_extensions}" \\')
self.assertEqual(self.lines[i + 1], self.assertEqual(self.lines[i + 1],