Skip to content

Basic Firewall 1.0.0-rc.5

Pre-release
Pre-release

Choose a tag to compare

@kanopi-code-bot kanopi-code-bot released this 30 Sep 21:26
· 12 commits to main since this release
cf6a38e

Fifth 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 ;params no
    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.html anywhere) and slightly
    narrower (a slug that merely starts with wp-login or wp-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.php limit 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 --lint may 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, /login before /LOGIN), naming the line that takes its
    requests: the cases firewall-check --lint reports from 2.35.1. The rule
    still saves.

  • The "verification cookie did not come back" notice in block mode
    (#46). A visitor challenged within two minutes of solving a challenge,
    carrying the <cookie_name>_solved marker but not the pass, is told why, as
    #37 did for exception mode. Decision_Dispatcher adds the line to the
    library's RequestChallenged event (kanopi/firewall 2.35.0), which the
    library renders above the form on its own page in block mode and puts in
    the render context's notices in exception mode, so both modes now share
    one implementation and the plugin no longer splices its own paragraph into
    the page. In block mode 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 new pass_cookie key, written when the name is not
    the default). The library's challenge.notice has 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, status could 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 than disabled, switched-off or
    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-report dumps both reports (JSON, or
    --format=yaml), and wp basic-firewall status gains 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 in block or
    exception mode, or an unanswered early verdict; recommended for an early
    path that did not evaluate, with the fix for the reason.

  • BASIC_FIREWALL_DEBUG adds an X-Basic-Firewall-Early response 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() as bucket/Class:index names (failed_rules; no
    messages) in the saved report, early-report, the status summary 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 with BASIC_FIREWALL_DEBUG on, 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_sampled says why a request asked (null otherwise), and the
    latest sample from each path is kept apart and shown by early-report
    (failed_rules_sample) and status ("Failed rules (last sample)"). Any failed rule makes the request a new failed-rules
    anomaly, which Site Health raises as critical in block or exception mode
    and as recommended in log mode. Reports now also carry anomalies, every
    anomaly a request had, most serious first.

  • A mismatch anomaly (#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_MODE and lockdown are reported under
    mode.overrides rather 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
    the DB_* credentials are. With it defined the compiled file holds no Redis
    password; the compiler records only where each connection's auth belongs,
    in the compile metadata and a redis-auth-paths.json sidecar (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 the not-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 --lint does 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's RateLimit::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 made wordpress.yml
    and search-bots.yml match 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.php or /wp-admin at the root and misses it under the prefix
    (equals, is one of, starts with, a regular expression, a rate limit
    pattern; ends with and contains pass), 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.php rate limit counts /WP-LOGIN.PHP too (#50). On a
    case-insensitive filesystem the server runs wp-login.php for any casing,
    and path keeps 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.php can 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 in PathSpellingTest, 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 strips AS at 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 written AS16509 is 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 whose wp-config.php required one
    autoloader above the snippet while a different vendor/ 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 own vendor/, the autoloader
    option, BASIC_FIREWALL_AUTOLOADER, a library already loaded (loaded),
    then dirname( ABSPATH ) . '/vendor' (site). The plugin's scoped copy
    still wins, and a named autoloader that cannot be read still stops the
    search with autoloader-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 writes Basic 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 the basic_firewall_private_path filter names a
    directory the plugin did not create, uninstall removes only the plugin's own
    files from it, and runtime.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_key and a few more; never a bare key), 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.