Releases: flowd/phirewall-preset-owasp-crs
Release list
0.5.1
0.5.1 - Match debugging metadata
This release makes blocked and tracked matches explainable: the match metadata now names the target a rule fired on and shows the value that triggered it. No breaking changes.
Match debugging metadata
Blocked requests carry two new MatchResult metadata keys alongside msg and owasp_log_data:
owasp_matched_variable- the target the first matching rule fired on, e.g.REQUEST_HEADERS:User-AgentorREQUEST_COOKIES:cart. The member name keeps the casing the client sent, so the log points at the real request data. On a fail-closed block it names the variable that failed closed.owasp_matched_value- the value the rule fired on, kept readable so the match can be understood and tuned: sanitized (control characters stripped) and length-bounded (200 bytes). Credential targets - cookie values andAuthorization-type headers (Authorization,Cookie,Proxy-Authorization,X-Api-Key,X-Auth-Token) - appear as[redacted]; the target name stays visible for tuning.
The PSR-3 per-match log context gains the same matched_value, redacted by the same single choke point, so both sinks stay consistent.
This turns an opaque block such as X-Phirewall-Owasp-Rule: 942340 into an actionable one: the metadata now shows owasp_matched_variable: REQUEST_COOKIES:cart, identifying the classic cookie false positive that can then be tuned away with excludeTarget('REQUEST_COOKIES:cart') instead of disabling the rule.
RuleMatch gained a public matchedValue property, appended after failClosed so positional construction against 0.5.0 keeps working.
Full Changelog: 0.5.0...0.5.1
0.5.0
0.5.0 - Anomaly scoring, false-positive tuning, match logging
This release replaces first-match blocking with CRS-style anomaly scoring, adds the tooling to tune false positives away instead of disabling rules, makes every match observable through PSR-3 logging, and hardens the engine against evasion and information leaks. The bundled rule snapshot was re-imported from CRS v4.29.0 and grew from 185 to 207 rules (196 CRITICAL, 10 WARNING, 1 NOTICE).
Anomaly scoring
Evaluation now follows the CRS anomaly-scoring model: every matching rule contributes its severity score (CRITICAL 5, ERROR 4, WARNING 3, NOTICE 2; a missing severity counts as CRITICAL), and the request is blocked once the accumulated score reaches the threshold (default 5, the CRS standard). In practice CRITICAL rules still block on their own; the 10 WARNING rules and the single NOTICE rule - classic false-positive sources such as the 942430 restricted-character checks - no longer block solo, which resolves the well-known utm_*/fbclid false positives. Fail-closed decisions (a capped variable, an oversized value, a PCRE error) block immediately regardless of the score.
Both presets accept an anomalyThreshold; anomalyThreshold: 1 restores block-on-first-match. Blocked requests carry full scoring metadata (owasp_anomaly_score, owasp_anomaly_threshold, owasp_rule_ids, owasp_log_data, owasp_fail_closed), and with diagnostics headers enabled the response reports X-Phirewall-Owasp-Rule (up to 10 ids, then ,+N) and X-Phirewall-Owasp-Score (score/threshold).
False-positive tuning
- Target exclusions remove a parameter from inspection instead of disabling a whole rule:
excludeTarget('ARGS:/^utm_/')globally,excludeTargetById(942431, 'ARGS:fbclid')per rule (CRSSecRuleUpdateTargetByIdstyle),excludeTargetByTag('attack-sqli', 'ARGS:comment')per tag. Exclusions are runtime configuration - never part of the compiled cache - and cannot lift the collection cap. - Value manipulators (
addManipulator()/addManipulatorById()) transform collected values before rules match, as the advanced escape hatch where excluding a whole parameter is too broad; returning an empty string removes the value. Documented with an explicit warning: manipulators weaken detection. - Both presets accept a
configure:closure receiving theCoreRuleSetMatcher, so exclusions, manipulators and rule toggles can be applied before the rules load.
Match logging (PSR-3)
Both presets and CoreRuleSetMatcher accept a logger. Every rule match is logged at info level with rule id, severity, score, paranoia level, matched variable and the rule's logdata: template expanded (%{TX.0}, %{MATCHED_VAR_NAME}, %{MATCHED_VAR}) - including sub-threshold matches on requests that pass, which is the signal for finding false-positive patterns before they block. Blocked requests additionally log a warning with the total score and all matched rule ids. New dependency: psr/log.
Expanded rule coverage
Named and negated rule-text selectors are honored: REQUEST_HEADERS:User-Agent collects exactly that header, and CRS exclusions such as !REQUEST_HEADERS:Cookie (16 shipped rules) now take effect, removing a source of cookie-driven false positives. The re-imported snapshot therefore includes previously dropped named-header and scanner rules (among them the 913100 scanner detection). REQUEST_URI_RAW targets are remapped to REQUEST_URI, and (?i) is injected into @rx patterns that relied on t:lowercase, so URL-path traversal and mixed-case gadget payloads no longer slip through.
Security hardening
- Oversized values fail closed. A single collected value longer than 2048 bytes is un-inspectable and blocks for every operator, closing a padding-based
@rxevasion and bounding worst-case regex backtracking. Configurable viasetMaxInspectableValueLength()for deployments with legitimately large single values. - Credentials are redacted from logs and metadata. Matched values of cookies and of
Authorization,Cookie,Proxy-Authorization,X-Api-KeyandX-Auth-Tokenheaders appear as[redacted]in expanded logdata; the target name is kept so the parameter stays identifiable for tuning. REQUEST_FILENAMEcollects the full request path instead of only the basename, so the restricted-files rules match/.git/config,/.envand similar probes again.@pmFromFileis confined and fails closed. Unsafe operands (directory traversal, absolute paths, stream wrappers, context escapes) are rejected and block deterministically instead of throwing - an exception would previously have disabled the whole rule set for that request under the default fail-open policy. Each data-file line is treated as one phrase (ModSecurity semantics), fixing a severe false positive where normal browser user agents were blocked as scanners.- The matcher honors the firewall's fail-open policy (
FailOpenAware): with fail-open disabled, an unexpected engine fault blocks the request instead of letting it through. bin/crs-importhardening: optional--sha256tarball verification, a reproducibility warning when no--tagis pinned, byte limits on downloads and archive entries, and theGITHUB_TOKENis only ever sent to api.github.com.
Quality assurance
The shipped rules are now guarded behaviorally from both directions: a benign corpus asserts realistic legitimate traffic (browser and bot requests, tracking parameters, JWT cookies, form logins) stays below the threshold at every paranoia level, and an attack corpus asserts realistic attacks across eight classes are blocked via their accumulated score. A rule-inventory diff gate fails the suite when a CRS re-import silently changes which rules ship, until the snapshot is deliberately regenerated. A known-evasions corpus pins the documented engine gaps (no raw JSON/XML body or upload inspection) so a change that closes one is surfaced. Five new runnable examples cover scoring, diagnostics headers, the fail-closed cap, credential redaction and tuning, and an Infection mutation-testing setup runs on a weekly schedule.
Breaking changes and upgrading
CoreRuleSet::match(): ?intwas replaced byevaluate(): RuleSetEvaluation; useanomalyThreshold: 1for the old first-match behavior.- Presets block at accumulated score >= 5 instead of on the first match; the fail2ban preset counts a request toward the ban only when that request itself reached the threshold.
VariableCollectorInterface::collect()returns named entries (list<array{name: ?string, value: string}>) instead of a flat value list.- A single value larger than 2048 bytes now blocks (previously it was partially inspected or passed); raise the limit via
setMaxInspectableValueLength()if you handle legitimately large single values. - The compiled-data cache schema was bumped; existing artifacts rebuild automatically.
- New dependency:
psr/log.
0.4.1
Phirewall OWASP CRS Preset 0.4.1
A patch release with a behavioral fix and a refresh of the bundled OWASP Core Rule Set. There are no breaking changes and no upgrade steps.
📦 Ruleset update
- OWASP Core Rule Set updated from v4.27.0 to v4.29.0. Automated import via the scheduled
CRS Updateworkflow; the rules are filtered and split per paranoia level as before. See the upstream CRS release notes for the rule-level changes.
🔧 Behavioral fix
- Nested cookie params no longer break
REQUEST_COOKIEScollection. PHP parses a bracketed cookie name (Cookie: foo[a]=1) into a nested array, exactly like query parameters. The collector cast every cookie value to string, so such a request raised an "Array to string conversion" warning (fatal under strict production error handlers) and the rules scanned the literal stringArrayinstead of the cookie payload. The collector now flattens cookie params to their scalar leaf values, matching howARGSalready handles nested parameters, so nested cookie payloads are scanned by the rules. Requests without nested cookies behave exactly as before.
What's Changed
- Flatten nested cookie params in REQUEST_COOKIES collection by @sascha-egerer in #9
- Move the CRS update test gate to the pull request CI by @sascha-egerer in #10
- Update OWASP CRS to v4.29.0 in #11
Full Changelog: 0.4.0...0.4.1
0.4.0
What's Changed
- Load CRS rules lazily through the compiled-data cache by @sascha-egerer in #6
- Prepare the 0.4.0 release by @sascha-egerer in #8
Full Changelog: 0.3.0...0.4.0
0.3.0
What's Changed
- Emit diagnostic_headers metadata for matched CRS rules by @sascha-egerer in #5
Full Changelog: 0.2.0...0.3.0
0.2.0
What's Changed
- Adopt phirewall 0.8 fail2ban block-on-match semantics by @sascha-egerer in #4
Full Changelog: 0.1.1...0.2.0
0.1.1
What's Changed
- Allow flowd/phirewall >=0.6 <1.0 by @sascha-egerer in #3
Full Changelog: 0.1.0...0.1.1
0.1.0
What's Changed
- Require the released flowd/phirewall ^0.6 by @sascha-egerer in #1
New Contributors
- @sascha-egerer made their first contribution in #1
Full Changelog: https://github.com/flowd/phirewall-preset-owasp-crs/commits/0.1.0