One week after Click2Shell shipped in WordPress 7.1.1, WordPress has pushed another core security release: 7.1.2, on 22 September 2026, fixing CVE-2026-87902, a CVSS 9.2 critical flaw that needs no account and no click. An unauthenticated attacker can make the site load a PHP file from outside its theme folders, and on some servers turn that into code execution. If you updated to 7.1.1 last week: that is not enough. Every branch back to 4.7 is affected, including 7.1.1 itself. Here is what the bug is, who is actually exposed, and what to do.
What it is
| Field | Value |
|---|---|
| CVE | CVE-2026-87902 |
| CVSS | 9.2 Critical (WordPress assessment) |
| Class | CWE-22 path traversal: unauthenticated local file inclusion, reaching code execution on some servers |
| Authentication | None. No account, no logged-in user action |
| Affected | 4.7.0 through 7.1.1 (every supported branch) |
| Fixed in | WordPress 7.1.2 (22 September 2026), backported to 4.7.37 |
| Exploited in the wild | No reports, no public PoC, not on the CISA KEV catalog as of 22 September |
| Reported by | Robert Ressl |
Unlike Click2Shell, where a logged-in administrator had to open a crafted link, this one is fully unauthenticated. Unlike wp2shell, which needed the REST batch API, this lives in the template loader every WordPress page request goes through.
Are you affected?
Read the branch table carefully, because the 7.1.1 cohort is the point:
| Branch you run | Update to |
|---|---|
| 7.1.x | 7.1.2 |
| 7.0.x | 7.0.6 |
| 6.9.x | 6.9.9 |
| 6.8.x | 6.8.10 |
| 6.7.x | 6.7.9 |
| 6.6.x | 6.6.9 |
| Older supported | Backported down to 4.7.37 |
If you patched to 7.1.1 in the last week for Click2Shell, you are still vulnerable: that release fixed a different bug and did not include this one. Auto-update sites will receive 7.1.2 on their own; verify the version actually running.
How the bug works
When WordPress renders a page, get_page_template() in wp-includes/template.php builds candidate template filenames and hands them to the template loader. One candidate comes from post meta and is protected; the other is built from the pagename query variable, which comes straight from the request. Here is the vulnerable branch, verbatim from 7.1.1:
if ( $pagename ) {
$pagename_decoded = urldecode( $pagename );
if ( $pagename_decoded !== $pagename ) { // 7.1.2 adds validate_file() here
$templates[] = "page-{$pagename_decoded}.php";
}
$templates[] = "page-{$pagename}.php";
}
Read the branches side by side. The meta-derived template is passed through validate_file(), WordPress's own traversal check, before it is accepted. The candidate built from pagename never was, and the extra urldecode() is what turns a traversal-shaped slug into a real path: a request value that arrives still percent-encoded (double-encoded on the wire, because PHP has already decoded the query string once) passes the inequality test, and the decoded candidate then contains real ../ sequences. Patchstack's analysis confirms this is the branch that matters. We verified the full chain in a filesystem harness against the 7.1.1 source before publishing: the working request resolves to
{active_theme}/page-{value}.php
which means the attacker does not control the whole path, only everything after the literal page- prefix. Three consequences define who is exposed:
1. The active theme must have a top-level folder whose name starts with page-. The page- prefix is fixed by the code, so the traversing path has to travel through a directory whose name starts with those five characters. Path resolution fails if that directory does not exist, which is exactly why this is not everyone. Some themes, including older default WordPress themes, ship such folders (the common page-templates/ convention fits), and the folder only needs to exist, not contain anything useful.
2. The included file must end in .php. The suffix is appended by the code, so the attacker picks any PHP file on the server.
3. The traversal depth must be exact. From wp-content/themes/{theme}/page-templates/, a standard docroot sits four ../ hops up; a fifth resolves above the docroot and misses the target. The payload value is templates/../../../../{target} with no page- repetition and no .php suffix of its own.
Loading a PHP file is local file inclusion, not automatically code execution: whatever that file does runs, nothing more. Reaching the attacker's own code needs a second condition on the server, which is the "some servers" in WordPress's advisory.
From LFI to RCE
Patchstack's analysis names the practical escalation: if PHP runs with register_argc_argv enabled, a known technique turns an attacker-chosen include into arbitrary code execution (the trick abuses a PHP file shipped with common PHP distributions that accepts arguments from the request when that setting is on, and writes attacker-controlled content to disk). That setting is off by default on PHP 8.5 but on by default on older PHP versions, so a surprisingly large installed base qualifies.
Put the whole chain together:
Unauthenticated request
| URL value reaches page-{value}.php unsanitized
v
Traversal escapes the theme directory (needs a page-* folder to resolve)
v
Attacker-chosen PHP file is loaded and executes
| only on servers with register_argc_argv on:
v
Distribution PHP file abused to write attacker PHP
v
Code execution under the web server account
At that point the attacker holds the site: wp-config.php credentials, the database, plugins, users, everything under the web server account.
What 7.1.2 actually changed
Two things, and the second says more than the advisory does.
First, the one-line fix: the pagename branch now applies the same validate_file() check the meta branch always had, so an encoded traversal is rejected before it becomes a candidate.
Second, 7.1.2 adds _wp_is_template_path_allowed(), a containment gate that every resolved template must now pass, regardless of which code path produced it: any template name containing .. must realpath() to a location inside the stylesheet directory, the template directory, theme-compat, or a child theme's direct parent. A one-line validation would have closed the reported hole; wrapping the whole loader means the security team treated template resolution as a class of problem, not a single bug. That is the response of a team that suspects the reported path is not the only one.
How Patchstack says to gauge your exposure
Neither of these is a fix, but both tell you how close to the worst case you are:
- Does the active theme have a top-level folder whose name begins with
page-? Checkwp-content/themes/{your-theme}/for anything matchingpage-*.page-templates/is the common one. - Is
register_argc_argvon? Check yourphp.ini. Off (the PHP 8.5 default) keeps the LFI from completing its final jump; on (default on older PHP) completes it.
Then update anyway, because the LFI alone is already serious: including an arbitrary core PHP file can change what a page outputs, expose behaviour meant to run only in specific contexts, and combine with other bugs.
Why this one matters even with zero attacks reported
Three reasons, and they compound:
- The patched-last-week cohort is exposed twice. Sites that rushed to 7.1.1 for Click2Shell learned the right habit from the wrong release; 7.1.1 is in the affected range for this one.
- No click, no login. Exploitation pressure on a flaw like this is internet-wide scanning, not spear-phishing. The exploitation curve follows the PoC, and one will appear.
- Detection is hard. A single crafted GET that loads a PHP file looks, in most access logs, like any other page request. There is no POST body, no obvious payload signature, nothing to grep for after the fact unless you logged the query string.
What to do
- Update to 7.1.2 (or your branch's matching release) now. There is no workaround; updating is the fix. Confirm the running version, not the dashboard's claim.
- Check the two exposure questions above and note the answers for your records: theme folder starting with
page-, andregister_argc_argv. - Set
register_argc_argv = Offif you do not rely on it (most sites do not). It removes the RCE jump for this bug and closes the same jump for every future LFI. - After patching, audit: unexpected files in
wp-content/anduploads/, new administrator accounts, and scheduled jobs (wp_cronentries) you did not create. If your logs showpagename=-style queries with traversal sequences before your patch time, treat the site as compromised and rebuild from a known-good backup.
Frequently Asked Questions
I updated to 7.1.1 last week. Am I safe?
No. 7.1.1 fixed the Click2Shell bug and five others, but CVE-2026-87902 shipped with 7.1.2 on 22 September. Every branch back to 4.7 was affected, including 7.1.1. Update again.
Do I need to worry about RCE, or only the file inclusion?
Run the two checks: a page-* folder in the active theme makes the LFI reachable at all, and register_argc_argv = On is what completes the jump to code execution on older PHP defaults. But treat the LFI itself as the emergency and update; the second condition is a detail an attacker can sometimes influence through other means.
Is this being exploited in the wild?
Not as of 22 September 2026: no reports, no public proof of concept, no CISA KEV entry. That will not necessarily last; for comparison, wp2shell went from disclosure to mass exploitation, and this one requires no authentication at all.
How do I know if I was attacked?
Look in access logs for requests where the pagename parameter contains traversal sequences (raw ../, or URL-encoded %2e%2e%2f and double-encoded %252e%252e%252f) or references to files outside the theme, dated before your 7.1.2 update. Also check for new admin users, unexpected files under wp-content/, and unknown scheduled jobs. Anything positive means: restore from a clean backup and rotate secrets.
Two WordPress core security releases in six days, and this one needs no authentication. This is the cadence your patch process has to survive. It is exactly what we watch for in managed detection, what we assume in a penetration test, and what we design against in secure web development. If you are not sure your sites made it to 7.1.2 before someone else noticed the release, talk to us and we will check with you.



