Skip to main content

WordPress shipped a fix for an unauthenticated path traversal today. CVSS v4 scores it 9.2, critical, and under the right conditions it ends in remote code execution. The advisory covers every branch from 4.7 upwards, which meant twenty-five separate releases going out in a single afternoon.

That is the headline. The part worth your team’s attention is different. Whether this was ever exploitable on your site was decided by the name of a directory inside your theme, and by a PHP setting that the official “production” configuration file does not actually turn off. Neither of those is a thing anyone treats as a security control. We went and measured both.

TL;DR

  • GHSA-7hp8-65ch-5whp: unauthenticated path traversal in WordPress page-template resolution, CVSS v4 9.2. Fixed in 7.1.2 and backported to 24 older branches, as far back as 4.7.37.
  • Three things have to line up for exploitation, and only one of them is WordPress core. The other two are your theme’s directory layout and your PHP configuration.
  • We downloaded the 100 most-installed themes in the WordPress.org directory and inspected their file listings. Sixteen carry the theme-side pre-condition, covering roughly 702,000 active installs.
  • All sixteen use the identical directory name, page-templates. A tutorial convention quietly became an attack pre-condition.
  • php.ini-production ships register_argc_argv commented out, so adopting the production config does not disable it.
  • Patch first. Then audit the pre-conditions, because the next bug of this shape will want the same three things.

A critical that runs through your folder names

The mechanics are short. When WordPress renders a page, get_page_template() builds a list of candidate template filenames from the URL, including one of the form page-{pagename}.php. Up to 7.1.1, the URL-decoded variant of that name was added to the candidate list without being passed through validate_file(), the function whose entire job is rejecting directory traversal. The undecoded variant was checked. The decoded one was not. That asymmetry is the bug, and the fix is the missing condition being added back.

On its own that gets an attacker the ability to make WordPress include a readable local .php file from outside the theme. Turning that into code execution needs two more things. First, the active theme, or its parent, has to contain a top-level directory whose name begins with page-, because that is the prefix the traversal has to start from. Second, there has to be a useful .php file on disk to land on. The advisory points at pearcmd.php, the long-standing PEAR-to-RCE stepping stone, which is reachable when register_argc_argv is enabled.

Three independent conditions. Core supplied one. Your theme author and your hosting stack supplied the other two, years ago, for reasons that had nothing to do with security.

We checked the hundred most popular themes

The advisory names Twenty Twelve, Twenty Fourteen, Neve, Hestia and Sydney as affected themes. That is a sample, not a survey, so we ran the survey. We pulled the 100 most-installed themes from the WordPress.org themes API, downloaded each package, and listed the top-level directories inside every one.

Sixteen of the hundred contain a top-level directory starting with page-. Together they account for roughly 702,000 active installs, about 8 per cent of the installed base covered by those hundred themes. The list, with WordPress.org’s own install estimates: Neve (200,000), Sydney (80,000), Hestia (70,000), Inspiro (60,000), Colibri WP (50,000), Twenty Fourteen (50,000), Twenty Twelve (50,000), ColorMag (40,000), Zakra (30,000), Spacious (20,000), Bloghash, Botiga, Tutor Starter, Silverstorm, Prespa and HybridMag.

The detail that makes this worth writing down: all sixteen use exactly the same directory name. Not one variant. Every single one is page-templates. That is not sixteen independent decisions, it is one convention, copied from theme to theme for a decade because it is the obvious place to keep custom page templates. Nobody chose a security posture. They chose a tidy folder, and the folder happened to start with the five characters that a traversal needed.

Two caveats we would rather state than have you infer. WordPress.org install counts are bucketed, so treat 702,000 as an order of magnitude, not an audit figure. And this covers directory themes only. Commercial themes and the bespoke builds most agencies actually ship are not in that sample, and in our experience they copy the same convention, so 16 per cent is a floor rather than a ceiling.

The other half of the pre-condition is PHP

Here is where it gets uncomfortable. register_argc_argv has a default of On in PHP, and it is only deprecated as of PHP 8.5. So the question is whether anything in a normal deployment turns it off.

We read the shipped configuration files. In php.ini-production, the line reads ;register_argc_argv = Off. It is commented out. The file goes as far as telling you that “for security reasons, this feature should be disabled for non-CLI SAPIs”, and then leaves the directive inactive, which means the compiled default of On survives. Moving from the development config to the production config, the single step most teams equate with hardening, changes nothing here.

The official PHP Docker image is worse again, and it is worse by accident. Its Dockerfile copies both php.ini-development and php.ini-production into the ini directory without activating either, so containers run on compiled defaults. The same Dockerfile builds PHP with --with-pear. The image therefore supplies the enabled directive and the target file in one layer. The advisory confirms the default cPanel configuration is affected too, on PHP below 8.5.

The patch is two patches

The core fix adds the missing validate_file() call. That closes this bug. The second half of the changeset is the one your architects should read: a new private function, _wp_is_template_path_allowed(), now sits inside locate_template() and resolves every candidate template with realpath() before loading it, confirming the true location sits inside the stylesheet directory, the template directory or wp-includes/theme-compat.

That is a different kind of defence. The first fix filters an input. The second enforces a containment invariant on the output, so the next unvalidated path into locate_template() from anywhere in core, a plugin or a theme fails closed rather than resolving. If you take one engineering lesson from today, take that one: filters catch the bug you found, invariants catch the class.

What to do this week

Confirm the version, do not assume it. Minor releases auto-update by default and this went out on every branch, so most sites are already fixed. The exceptions are predictable: sites with auto-updates disabled, and Composer-managed installs where core is a version constraint that no automation bumps. If you run Bedrock or johnpbloch/wordpress, nothing happened on its own tonight.

Check the theme condition in one command. Run ls -d wp-content/themes/<theme>/page-* against your active theme and its parent. Most agencies can answer this for every client site in ten minutes, and the answer is worth keeping.

Check the PHP condition against the web SAPI, not the CLI. The directive is ignored for CLI, so php -i at a shell prompt will mislead you. Read it through the runtime that serves requests, and if it is on and you do not need it, set it off in a conf.d file rather than editing the shipped ini. While you are there, if nothing on the box uses PEAR, remove pearcmd.php.

Then write it down. Both pre-conditions are single-line checks that can live in CI and run on every deploy, and neither depends on anyone remembering today. About 44 per cent of WordPress installs are not on the current branch, and roughly 1 per cent are on branches so old they received no fix at all. Backports bought those sites time. They do not buy the next one.

The wider point

Severity scores describe the bug. Pre-conditions describe you. A 9.2 that needs a specific folder name and a specific ini flag is a different risk on a stock block theme than it is on a ten-year-old build running Twenty Fourteen in a container, and no scanner reading version numbers will tell you which one you are. The teams that answered this quickly today are the ones that already knew what their themes contain and what their PHP does.

We build and maintain WordPress platforms, and we run the DevOps behind them. If you would like someone to answer the theme question and the PHP question across your estate rather than one site at a time, get in touch.

📷 Photo by Maksym Kaharlytskyi on Unsplash