Repository navigation
v2.35.0 — rules that said one thing and did another
v2.35.0
Rules that said one thing and did another.
Eight reports from the WordPress and Drupal integrations. Each was a place where a rule read correctly and the firewall did something else: an ASN rule that never matched, a WordPress rule that stopped at the site root, a rate limit that counted one path under several spellings, a pairing the docs recommended that never ran. One addition: a host can now say something on the challenge page.
PHP 8.1–8.5 supported. One new configuration key, challenge.notice, and it's opt-in. Several fixes change what an existing config does, so read What changes on upgrade.
What changes on upgrade
- ASN equality rules start matching.
asn:16509,asn:AS16509andasn@in:…never matched before, so a block rule on a network starts blocking it.asn@not_equals:Xstops matching network X. It used to match every visitor, including X. Check anynot_equalsASN rule. - The path every rule sees is normalised. A doubled slash, a
.segment (raw or%2e), percent-encoded unreserved characters and;paramsno longer change what a rule matches...is kept as written. Logs and block records show the normalised path. presets/wordpress.ymlandsearch-bots.ymlare at Preset-Version 2. WordPress's paths match at any depth. They're wider (subdirectory installs, core in its own directory,readme.htmlanywhere) and slightly narrower: a post slug that only starts withwp-loginorwp-adminis no longer blocked. See presets/CHANGELOG.md.- Rate-limit paths ignore case, as URL rules already did.
/wp-login.phpnow also limits/WP-LOGIN.PHP. A pattern written as a regex keeps its own flags. - Redirect targets with a raw control character or whitespace go to
/, as does anything a browser would read as//. firewall-check --lintmay report a new warning on a config it used to pass: Rule "X" has more than one entry for /login; only the first ever runs. That entry never ran.- The log line's
urlfor a directly served file no longer ends in a stray/.
Added
-
challenge.noticeandRequestChallenged::addNotice()(#421, #435). Inmode: blockthe challenge page is the library's, so a host had no way to tell a visitor anything. That includes the case that asked for this: a pass cookie that never comes back, so the visitor is challenged again, forever, with nothing on the page to say why.challenge: notice: "Having trouble? Email help@example.com." # or a list
For a line that depends on the request, add it from a listener. For example: marker cookie present, pass absent → "Your browser did not send back the verification cookie". Notices are plain text, escaped, and shown above the form on every built-in provider's page. Custom providers get them as
noticesin the render context, and so doesmode: exceptionthroughgetRenderContext(). See A notice on the challenge page.
Fixed
-
ASN rules compared a number with text (#423, #427). Every documented ASN equality rule had never matched:
asn.md,geoip-setup.md,challenges.md,tarpit.md, and rule-source templates. For a client in AS16509:Rule Before After asn:16509,asn:AS16509no match match asn@in:13335,16509no match match asn@not_equals:16509match no match asn@contains:1650match match (text operator, unchanged) -
//wp-jsonand similar spelled around a rule (#425, #429). Underpath_source: pathinfo,pathcame from the raw request URI, while the server normalised the URL before routing it toindex.php. WordPress trims every leading slash, so//wp-json/wp/v2/userswas the REST API to it and missedpath@starts_with:/wp-json/. Both path sources now normalise: repeated slashes collapse,.segments go, unreserved characters are decoded (%2Fstays encoded), and;paramsare dropped...is kept as written, on purpose. Resolving it removes the segment before it, so/wp-json/wp/v2/…/../../../xwould have been/xto the rules and still the REST API to WordPress. That would have added a way around the very rule this fixes. It was caught in review before release. -
A solved challenge could redirect off-site (#422, #428, #433).
sanitizeRedirect()accepted anything that started with one slash and not//or/\. A browser drops tab and newline from a URL, so/\t/evil.examplebecame//evil.example. Inmode: exceptionthat reached theLocationheader, where PHP's header check refuses CR and LF but not tab. Raw control characters and whitespace are now refused anywhere, and\counts as/in the path. Percent-encoded forms such as/search?q=a%20bare kept, because a browser never decodes them when working out the host. -
WordPress's presets stopped at the site root (#420, #434). With core in its own directory (
/wp/wp-login.php), or in a subdirectory install,wordpress.ymlnever fired, andsearch-bots.yml's crawler allow covered/wp/wp-login.php. Both now match at any depth, on a segment boundary, so/2026/wp-admin-tips/is a post and not the admin. A subdirectory install still setsbase_pathfor requests routed through itsindex.php. -
The documented account-plus-address rate-limit pair never worked (#424, #430). A rate limit uses the first entry in its
config:whose path matches, so the address-keyed entry the docs put second never ran, and--lintcounted it as coverage. The linter now reports the unreachable entry and judges coverage across separate rules. The docs show the pair as two rules. -
Rate-limit patterns were case-sensitive (#426, #432). URL conditions already ignored case, but a limit on
/wp-login.phpgave/WP-LOGIN.PHPits own budget on a case-insensitive filesystem. They now agree. Thepathkey component is lower-cased too, and a lower-case path keys exactly as before. -
The logged URL of a direct file had a trailing slash (#419, #431). The log line used
getUri(), which gave/wp-login.php/, beside a block record that said/wp-login.php. Both now come from one helper.
Docs
configuration-keys.mdlistedchallenge.cookie_nameandheader_namewith a default of''. They'refw_challenge_passandX-Firewall-Challenge. On Pantheon, startcookie_namewithSTYXKEY_, or the edge strips it.- The
mode: exceptionexample now says the pass cookie's name must matchchallenge.cookie_name.
Full changelog: v2.34.1...v2.35.0