Skip to content

v2.34.0 — the path the server ran

Choose a tag to compare

@sean-e-dietrich sean-e-dietrich released this 30 Sep 00:13
· 33 commits to 2.x since this release
7499821

v2.34.0

The path the server ran.

A file the web server runs directly, such as WordPress's wp-login.php, xmlrpc.php and every /wp-admin/*.php, or Drupal's core/install.php, was matched as /. So the WordPress preset never fired on the pages it names, a /wp-login.php rate limit never counted, and a negated path condition matched every admin screen. Nothing reported it, and firewall-check said the rules matched. global.path_source: script_name fixes it for the sites that need it.

PHP 8.1–8.5 supported. The new keys are opt-in, and the default is unchanged. One dependency minimum goes up; see What changes on upgrade.

What changes on upgrade

  • Nothing about path, unless you set path_source. pathinfo stays the default in 2.x, because switching would change what path means on sites that work today.
  • matomo/device-detector is now required at ^6.5.2, up from ^6.4. 6.5.2 adds Nikto to its bot database, so a bot:true rule now matches Nikto on every install, rather than depending on which version happened to be installed. PHP support is unchanged (^7.2|^8.0). A site on 6.5.1 would have got this on its next composer update anyway.
  • Block records store a direct file's URL correctly. A record for /wp-login.php used to store https://host/wp-login.php/, with a trailing slash. This applies whatever path_source is.
  • firewall-check prints a note on stderr when --url names a .php file other than index.php and there's no --script-name. stdout, and so --json, is unaffected.
  • The bot: notice no longer lists Nikto among what bot: misses.

Which setting you need

Application path_source
WordPress script_name
Drupal, for rules on core/install.php, core/rebuild.php or core/authorize.php script_name
Laravel, Symfony, Drupal's routes: anything where every request goes through index.php the default, pathinfo
global:
  path_source: script_name
  base_path: /blog        # only if the site is installed in a subdirectory

script_name gives the same answer as pathinfo on a front-controller app. It isn't "more correct" there, so leave it off.

Added

  • global.path_source (#414, #415). pathinfo (default) is Symfony's getPathInfo(), the path relative to the front controller. script_name is the file the server ran: SCRIPT_NAME + PATH_INFO, falling back to getPathInfo() when that file is the front controller. It feeds every path condition, rate-limit pattern and preset, and the path in logs, block records and {{request.path}}.

    Request Server runs pathinfo script_name
    /wp-login.php wp-login.php / /wp-login.php
    /wp-admin/edit.php wp-admin/edit.php / /wp-admin/edit.php
    /wp-admin/ wp-admin/index.php / /wp-admin/index.php
    /learning/ index.php /learning/ /learning/

    It uses the file that ran, deliberately, not the raw URL. The server decodes and normalises the URL before it chooses a file. /./wp-login.php, /%77p-login.php, //wp-login.php, /x/../wp-login.php and /wp-login.php;x all run wp-login.php, and a raw-URL match would have let each of them past a path:/wp-login.php rate limit. Under script_name, they're all /wp-login.php and count against the same budget. There's a test for each spelling.

    It's read from the request being evaluated, never from $_SERVER, so it's right under Octane, RoadRunner and Swoole.

  • global.base_path, for a subdirectory install. On a direct-file request nothing says where the application starts (getBasePath() for /wp-admin/edit.php is /wp-admin), so it's named: <base_path>/index.php is the front controller, and base_path comes off the front of any other file's path, at a segment boundary only. /blog/wp-login.php then matches the presets as /wp-login.php.

  • firewall-check --script-name=FILE checks a request the way the site receives it. Without it, the tool matches the path as typed, which is how it came to report WordPress rules as matching when they never did. The output adds a matched as path line when the path differs from the URL, and --json's request.path is the path the rules saw.

  • firewall-doctor reports which path source is in use. An unknown path_source or an unusable base_path is an error. At runtime, the firewall falls back to pathinfo with a warning, the same way an unknown mode falls back to block.

Fixed

  • presets/wordpress.yml, search-bots.yml and drupal.yml never saw direct-file requests under the documented integration. With path_source: script_name they do. The search-bots.yml exclusion was the worst of them: it's a negated path match, so matched against / it was always true, and the crawler allow covered exactly the back end it was written to keep out. The presets now say so in their headers; that's a comment change only, so no Preset-Version bump.
  • The WordPress integration docs now set path_source: script_name. The Drupal, Symfony and Laravel sections say to keep the default. Path Source has the full explanation.
  • CI broke on the device-detector 6.5.2 release (#416). composer.lock isn't committed, so CI resolved 6.5.2 as soon as it shipped, and three tests used Nikto as their example of an agent bot: misses. Raising the minimum makes that the same everywhere.

Not in this release

  • pathinfo has a related raw-URL weakness through index.php: /./user/login doesn't match a rule on /user/login. It needs a different fix, normalising the default source.
  • /WP-LOGIN.PHP on a case-insensitive filesystem.
  • Downstream: kanopi/firewall-laravel and kanopi/firewall-symfony forward options to firewall-check explicitly, so --script-name needs adding there before their users can pass it.

Full changelog: v2.33.2...v2.34.0