Skip to content
PROTECT MY WP

The PHP Setting That Turned a Core Bug Into a Shell

By 5 min read

WordPress 7.1.2 went out on 22 September with a single fix, CVE-2026-87902, rated 9.2 out of 10. The bug had been in core since 4.7.0, released in December 2016, and the fix was backported to twenty-four older branches, down to 4.7.37. Patchstack's sensors saw the first probing requests at 11:49 UTC, under four hours after the release, and the first attempt to write a file to disk at 15:34. By the next day a Nuclei template was circulating and the traffic peaked.

What decided whether a site actually got a shell was not WordPress at all.


What the bug does

When WordPress renders a page it works out which theme file to use, and get_page_template() builds a list of candidates from the page's slug. The slug comes from the pagename query parameter. The sanitiser that cleans it rewrites literal dots and cuts at literal slashes, which is correct, but it leaves percent-encoded ones alone, so %2e%2e%2f survives. Later in the chain that gets decoded, and the candidate path walks out of the theme directory.

The result is that an unauthenticated request can make WordPress include any PHP file the web server can read. On its own that is a file inclusion bug, which is serious but bounded. It needs two things to be true: the request must carry a page_id that resolves to a real page, or WordPress serves a 404 before it ever gets to template resolution, and the active theme must have a top-level directory starting with page-, such as page-templates, which plenty of classic themes do. That combination of page_id and a pagename containing an encoded traversal is also the detection signal, because the two parameters almost never appear together in real traffic.


Where it turns into code execution

Including a file you did not write only runs code the attacker already controls if there is a file on the server that does something useful when included with the right arguments. There is a well-known one: pearcmd.php, the command-line front end for PEAR. It lives at /usr/local/lib/php/pearcmd.php on the official Docker PHP images and at /usr/share/php/pearcmd.php or /usr/share/pear/pearcmd.php on various distributions. Given the right arguments, PEAR's config-create subcommand will write a file of the attacker's choosing to a path of the attacker's choosing.

But pearcmd.php reads its arguments from $argv, and a web request does not have an $argv. Unless register_argc_argv is on. With that setting enabled, PHP takes the query string and exposes it to the included script as if it were command-line arguments. The attack chain Patchstack documented was three stages: include a harmless core file to confirm the traversal works, include pearcmd.php with +config-show to confirm PEAR is present and arguments are getting through, then +config-create with a PHP payload and a path under /tmp. Files they saw written included poc87902.php and wp-pear-rce-flag.php.

So the server configuration decided the outcome. With register_argc_argv off, the same request against the same unpatched WordPress reads a file and stops. With it on, the attacker has a shell. Patchstack put it plainly: disabling the setting is the difference between an information leak and code execution.


Why it was on in the first place

register_argc_argv exists so command-line scripts can read their arguments. It has no purpose in a web request. PHP's own php.ini-production sets it off, and the php.ini that Debian and Ubuntu ship for FPM sets it off, which is why most VPS setups were only exposed to the file read.

The official Docker images are different. They ship with no php.ini at all, just the two templates sitting next to where one should go, and in the absence of a php.ini PHP falls back to its compiled-in defaults. The compiled-in default for register_argc_argv is on. Those same images ship pearcmd.php. A WordPress container built on php:8.3-fpm without someone explicitly copying php.ini-production into place had both halves of the chain by default, and a great many containers are built exactly that way.

Check yours with the FPM binary, not the CLI, because they can read different ini files:

php-fpm8.3 -i | grep register_argc_argv

Substitute your version. If it says On, set it off in php.ini or the pool configuration and restart FPM. If you are not using PEAR, which you almost certainly are not, delete pearcmd.php while you are there. The uploads rule from the Elementor post would not have helped here, since nothing was uploaded. The file was written to /tmp by PEAR, so the thing to check on a server that was unpatched on 22 or 23 September is:

find /tmp /var/tmp -name "*.php"

Anything that comes back means stage three succeeded and the server is compromised, not just the site.


Why one ini line mattered

Minor auto updates put 7.1.2 on most sites within hours, which is what they are for. But hours was the window the attackers were working in, and a lot of sites run with auto updates off for reasons that seemed good at the time. For those sites, one php.ini line that had been set correctly for a decade meant a bug that was exploited the same day it was published did nothing worse than read a file.

That is the general pattern I keep coming back to. The patch is the fix. The server configuration is what decides how much the gap before the patch costs you.


Where this fits

Chapter 1 of the Protect My WP handbook now includes register_argc_argv in the PHP settings list, with the Docker default explained, alongside the other settings that limit what an attacker can do with a bug you have not patched yet. Chapter 2 uses 7.1.2 as the current example of why core point releases apply themselves.

Get the book for £19.

More on this topic

Want to go deeper?

Protect My WP is the whole handbook: 14 chapters from the server up, kept current as WordPress changes, with a PDF edition included. £19, one payment, every future update. Not sure yet? The foreword and Chapter 1 are free, and the shorter guide walks the same ground faster.