Repository navigation
Basic Firewall 1.0.0-rc.5
Pre-releaseFifth release candidate for 1.0.0. Published as a GitHub pre-release, so
the releases/latest/download URL does not serve it; install it by its own URL.
It adds diagnostics for the wp-config.php early path, readable from WP-CLI and
Site Health, so a host where the web containers behave differently from WP-CLI
(#34) can be diagnosed: the last web request's report and the last anomaly,
failed rules and mode/compiled-file mismatches, and every fail-open logged
(rate-limited) to the PHP error log. It keeps credentials out of the Advanced
YAML box and exports, and lets the Redis password stay out of the compiled
file through BASIC_FIREWALL_REDIS_PASSWORD (#47, #48). The early path no
longer loads a second Composer autoloader when the library is already loaded
(#44). It bundles kanopi/firewall 2.35.1, which changes what some existing
rules do (ASN equality, path normalisation, presets at any depth, rate-limit
case): read What changes on upgrade first. The "verification cookie did not
come back" notice now shows in block mode too (#46). What changed since
1.0.0-rc.4 is below.
What changes on upgrade
This release bundles kanopi/firewall 2.35.1. Several of its fixes change what
an existing configuration does:
- ASN equality rules start matching. An ASN rule with is equal to or
is one of (16509,AS16509) never matched before; a block rule on a
network now blocks it. An ASN is not equal to X rule stops matching
network X — it used to match every visitor, X included. Check your ASN
rules, especially negated ones. - The path every rule sees is normalised. Doubled slashes,
.segments
(raw or%2e), percent-encoded unreserved characters and;paramsno
longer change what a rule or rate limit matches;..is kept as written.
The log and block records show the normalised path. - The WordPress presets are at Preset-Version 2.
search-bots.yml(and
wordpress.yml, which this plugin does not offer on a WordPress site)
match WordPress's paths at any depth: wider (a subdirectory install,
core in its own directory,readme.htmlanywhere) and slightly
narrower (a slug that merely starts withwp-loginorwp-admin, such
as/wp-login-help/, is no longer treated as the login or the admin). - Rate-limit paths ignore case, as URL rules already did: a
/wp-login.phplimit also counts/WP-LOGIN.PHP. A pattern written as a
regular expression keeps its own flags. - Redirect targets with a raw control character or whitespace go to
/
after a solved challenge, as does anything a browser would read as//. firewall-check --lintmay warn about a rate-limit entry that never
runs because an earlier entry in the same rule covers it. The rule screen
now says the same.- The logged URL of a directly requested file loses its stray trailing
/(/wp-login.php, not/wp-login.php/), matching the block record.
Added
-
The rule screen warns about a rate-limit line that can never run
because an earlier line in the same rule covers it (/log*before
/login,/loginbefore/LOGIN), naming the line that takes its
requests: the casesfirewall-check --lintreports from 2.35.1. The rule
still saves. -
The "verification cookie did not come back" notice in
blockmode
(#46). A visitor challenged within two minutes of solving a challenge,
carrying the<cookie_name>_solvedmarker but not the pass, is told why, as
#37 did forexceptionmode. Decision_Dispatcher adds the line to the
library'sRequestChallengedevent (kanopi/firewall 2.35.0), which the
library renders above the form on its own page inblockmode and puts in
the render context'snoticesinexceptionmode, so both modes now share
one implementation and the plugin no longer splices its own paragraph into
the page. Inblockmode the library issues the pass itself, so the
dispatcher also sets the marker when the library announces the solve. Both
evaluation paths: the wp-config.php path reads the pass cookie's name from
the runtime sidecar (a newpass_cookiekey, written when the name is not
the default). The library'schallenge.noticehas no Challenge-screen field
yet; the Advanced YAML can set it. -
Diagnostics for the early path, readable from anywhere (#34). Every
status screen described its own request, and WP-CLI's is not a web request:
on a host that runs WP-CLI in its own container,statuscould report the
early path evaluating while the web containers were not. The runner now
saves a compact report of the last web request that reached WordPress, and
separately of the last anomalous one (a fail-open, an early path that did not
evaluate for a reason other thandisabled,switched-offor
deferred-login, or a refusal the early path handed on instead of
answering), in transients kept for a day and written at most once every five
seconds; WP-CLI and cron are never recorded. Each report carries both paths'
view of the request (called, evaluated, reason, autoloader, verdict,
failure with file:line), the mode each path's firewall was actually in, the
library copy and version, the compiled file's path, modification time and
hash prefix, and the cache backend on each path. No query string, cookies,
headers or client address. -
wp basic-firewall early-reportdumps both reports (JSON, or
--format=yaml), andwp basic-firewall statusgains Last web request
and Last anomaly rows. -
Site Health raises a recent anomaly (under six hours old) on the
evaluation check: critical for a request let through unfiltered inblockor
exceptionmode, or an unanswered early verdict; recommended for an early
path that did not evaluate, with the fix for the reason. -
BASIC_FIREWALL_DEBUGadds anX-Basic-Firewall-Earlyresponse header
with the compact report, on responses the early path writes and on requests
that reach WordPress. Troubleshooting only, and off by default: it tells
anybody who can make a request how the firewall is deployed. -
Rules that failed to construct, per path (#41). The library skips a rule
whose constructor throws and carries on, so a rule failing only on the web
containers showed up nowhere. Both paths now record their firewall's
getFailedRules()asbucket/Class:indexnames (failed_rules; no
messages) in the saved report,early-report, thestatussummary and the
debug header. Sampled rather than asked on every request, because answering
builds every rule and so defeats the library's lazy construction: each path
asks withBASIC_FIREWALL_DEBUGon, on a request that failed open, or at
most once a minute per container (a marker file, one stat when not due).
The trade-off is that an intermittent failure can fall between samples.
failed_rules_sampledsays why a request asked (nullotherwise), and the
latest sample from each path is kept apart and shown byearly-report
(failed_rules_sample) andstatus("Failed rules (last sample)"). Any failed rule makes the request a newfailed-rules
anomaly, which Site Health raises as critical inblockorexceptionmode
and as recommended inlogmode. Reports now also carryanomalies, every
anomaly a request had, most serious first. -
A
mismatchanomaly (#41). The report recorded the configured mode, the
mode each path ran and each path's view of the compiled file, but a
disagreement was not an anomaly, so the next ordinary request overwrote it.
It is now flagged when a path's mode differs from the one the last compile
wrote, or the compiled file's hash differs between the paths or from the last
compile's — the signature of a stale copy on a container that did not do the
compile — and kept in the anomaly slot; Site Health recommends a rebuild. A
panic file,BASIC_FIREWALL_MODEand lockdown are reported under
mode.overridesrather than flagged. The compile now records the hash prefix
and mode of what it wrote in its meta, and the early path records the
compiled file's hash prefix alongside its modification time. -
BASIC_FIREWALL_REDIS_PASSWORD(#48): the password for every Redis
connection — block list storage and rate limit counters — supplied from
wp-config.php and injected at request time on both evaluation paths, the way
theDB_*credentials are. With it defined the compiled file holds no Redis
password; the compiler records only where each connection'sauthbelongs,
in the compile metadata and aredis-auth-paths.jsonsidecar (paths and ACL
usernames, never a password). The Storage screen shows the field as set in
wp-config.php while it is defined, and the rate limit editor says it is
overridden. -
Site Health: "Basic Firewall Redis password" (#48). Recommended when a
Redis password is written into the compiled file in plain text (define the
constant or use%env()%), or when the constant was defined after the last
rebuild; critical when the file was built with the constant and it has
since been removed.
Changed
-
Requires and bundles kanopi/firewall 2.35.1 (from 2.34.1). Several of
its fixes change what an existing configuration does; see What changes on
upgrade above. -
fail-open (early|runner)log lines are rate-limited (#41). They were
written on every request, so a persistent failure on a busy site logged one
line per request. The first is still always written; after that, at most one
line a minute per exception class and file:line, and the next line written
says how many were held back (… (N more since <time> UTC)). The state is
kept in a marker file in the private directory, as thenot-evaluated
warning's is, so the limit holds across workers and containers; a marker
that cannot be written means logging every time. The report still records
every failure.
Fixed
-
The rate-limit pairing check judges coverage by the line that runs, as
firewall-check --lintdoes from kanopi/firewall 2.35.1. On save and in
Site Health, an identity-keyed line that an earlier line in its rule covers
is no longer reported as unpaired (it never runs); a companion is the line
of another enabled rule that actually takes the request, found with the
library'sRateLimit::patternToRegex(); paths are compared without regard
to case; and a disabled rule never counts as coverage. -
WordPress in its own directory: presets fire, and Site Health flags only
the site's own rules (#45). kanopi/firewall 2.35.0 madewordpress.yml
andsearch-bots.ymlmatch WordPress's paths at any depth, so the crawler
allow no longer covers/wp/wp-login.php. Site Health's request path
check no longer flags every enabled preset: it asks each of the site's own
enabled rules whether it matches/wp-login.php,/xmlrpc.php,
/wp-cron.phpor/wp-adminat the root and misses it under the prefix
(equals,is one of,starts with, a regular expression, a rate limit
pattern;ends withandcontainspass), and lists each by name. An
enabled preset's rate limit on a bare core path, such as
rate-limiting.yml's/wp-login.php, is still named, since that preset
is unchanged and rate limit patterns are anchored. -
A
/wp-login.phprate limit counts/WP-LOGIN.PHPtoo (#50). On a
case-insensitive filesystem the server runswp-login.phpfor any casing,
andpathkeeps the client's; rate-limit patterns now ignore case, as URL
rules already did. Tests cover both, with the server values such a server
sends. -
Paths routed through
index.phpcan no longer be spelled around a rule
(#51). The library normalises the path every rule sees, on both path
sources, so//wp-json/…,/./wp-json/…,/%2e/wp-json/…,
/%77p-json/…and/wp-json;x/…match a rule or rate limit on
/wp-json/, as the direct-file spellings of #32 already did...is kept
as written. Covered inPathSpellingTest, over HTTP on the early path and
end to end. -
ASN rules are compared by the library as numbers, and the plugin's
workarounds are gone (refs #49). The type no longer casts a typed number
to an integer or stripsASat compile time, and a list of numbers compared
with is equal to, is one of or is not equal to is no longer compiled
into a digits-only#^16509$#pattern: the library reads both sides as a
number now. A list entry writtenAS16509is matched instead of skipped.
Stored rules need no change. (Latitude/longitude equals is not part of
this release.) -
The block list screen and WP-CLI resolve a
%env()%Redis password
rather than sending the token itself as the password (#48). -
The early path no longer requires a second Composer autoloader when the
library is already loaded (#44). A site whosewp-config.phprequired one
autoloader above the snippet while a differentvendor/sat beside the
WordPress root had that second one required too, so classes could resolve
from either tree. "Library already loaded" is now asked before the fixed
site locations: the order is the plugin's ownvendor/, theautoloader
option,BASIC_FIREWALL_AUTOLOADER, a library already loaded (loaded),
thendirname( ABSPATH ) . '/vendor'(site). The plugin's scoped copy
still wins, and a named autoloader that cannot be read still stops the
search withautoloader-unreadable. -
A firewall failure that fails open is now logged. Anything other than a
verdict that made either evaluation path let a request through — the library
failing to start, or throwing partway through evaluating — was recorded only
for Site Health on the request it happened on, which is a visitor's, so it
left no trace (#34). Each now writesBasic Firewall [warning]: fail-open (early)or(runner)to the PHP error log, with the exception class, its
message (credentials in a URL masked) and where it was thrown. The request
still goes through: failing open is the design, failing silently was not. -
An early path that is called and does not evaluate is logged, at most
once every 15 minutes per reason across every web container (a marker file in
the private directory):Basic Firewall [warning]: not-evaluated (early). -
Uninstall removes the runtime sidecar from a pre-existing private
directory (#43). Where thebasic_firewall_private_pathfilter names a
directory the plugin did not create, uninstall removes only the plugin's own
files from it, andruntime.json(written when the firewall is switched off
in the admin or its mode pinned) was missing from that list, so it outlived
the plugin. A directory the plugin created was always removed whole.
Security
- Credentials in the Advanced YAML are no longer shown or exported (#47).
The box was a free-form pass-through, outside the redaction #17 and #23
applied, so a Redis password, API key, token or DSN typed there went back
into the page and into every export in the clear. A value under a
credential-shaped key (*password*,*secret*,*token*,auth,
api_keyand a few more; never a barekey), a credential header, and the
password in a URL are now shown as[redacted], and%env()%/%file()%
tokens stay visible. A placeholder saved back unchanged keeps the stored
value; one that moved, or whose host changed beside it, refuses the save
rather than storing[redacted]. An export masks them and lists each as
advanced_yaml: <path>; an import restores them from the receiving site's
own block or drops and reports them, and names any credential an imported
block carries in the clear. The Compiled screen hides them too. - The Redis password can be kept out of the compiled file (#48), which
lives under uploads and is web-readable on some servers: define
BASIC_FIREWALL_REDIS_PASSWORD(injected at runtime, never written) or use a
%env()%token (only the name is written). A password typed literally is
still written, because the wp-config.php path cannot read the settings, and
Site Health now recommends against it.