WordPress 7.1.2 Patches an Unauthenticated File-Inclusion Bug That Reaches Back to 2016
WordPress 7.1.2 closes an unauthenticated local file inclusion bug in page template resolution that can reach remote code execution, present in WordPress Core since 2016.
WordPress 7.1.2 shipped on September 22, 2026, and it fixes exactly one thing: an unauthenticated local file inclusion bug in page template resolution that can be chained into remote code execution. WordPress’s own security team calls it a critical severity vulnerability, and the code path it lives in has been part of WordPress Core since version 4.7.0, released in December 2016. No login is required to trigger it.
Table Of Content
- The Bug Sat Three Lines Below Its Own Fix
- What It Takes to Turn File Inclusion Into Code Execution
- The Patch Does More Than Close the One Hole
- A CVSS Score That Depends on Who’s Scoring It
- Every Supported Branch Back to 2016 Gets the Fix
- WordPress Core’s Third Critical Release in Two Months
- What to Do
The bug, tracked as CVE-2026-87902, was reported to WordPress’s HackerOne-run bug bounty program by a researcher named Robert Ressl. It was fixed alongside a broader defensive change that WordPress’s team added on top of the specific patch, which is worth understanding on its own.
The Bug Sat Three Lines Below Its Own Fix
WordPress builds the list of candidate template files for a page inside get_page_template() in wp-includes/template.php. Security researchers at Patchstack, who published a technical breakdown the same day, laid out the vulnerable code:
// wp-includes/template.php, get_page_template(), WordPress <= 7.1.1
if ( $template && 0 === validate_file( $template ) ) {
$templates[] = $template;
}
if ( $pagename ) {
$pagename_decoded = urldecode( $pagename );
if ( $pagename_decoded !== $pagename ) {
$templates[] = "page-{$pagename_decoded}.php";
}
$templates[] = "page-{$pagename}.php";
}
Look at the two branches side by side. The first one runs the template slug through validate_file(), WordPress’s own directory-traversal check, before accepting it. The candidate built from the pagename query variable, three lines below, never gets that same check. The protection existed in the function; it just wasn’t applied everywhere it needed to be.
Two details shape what an attacker can actually reach with this. The filename is assembled as "page-{$pagename}.php", so a traversal payload has to continue a path that genuinely starts with page-, and the target still has to end in .php because the extension is hard-appended. That’s why the practical precondition is an active theme that ships a top-level directory starting with page-, something like a page-templates folder. According to the GitHub Security Advisory for the fix, that describes several themes still in real-world use, including Twenty Twelve, Twenty Fourteen, Neve, Hestia, and Sydney. The added urldecode() call on the pagename value is what turns an otherwise inert traversal-shaped slug into a real filesystem path once it’s decoded.
What It Takes to Turn File Inclusion Into Code Execution
Including a local PHP file isn’t, by itself, code execution of an attacker’s choosing. It runs whatever that file already does. Getting from inclusion to arbitrary code requires a readable .php file already sitting on the server that behaves usefully once included, and Patchstack points to the well-known candidate: PEAR’s pearcmd.php, which only becomes exploitable when PHP is running with register_argc_argv enabled. That setting is on by default in the official PHP Docker images and in cPanel environments running PHP versions below 8.5, which is a much larger share of real hosting than “unusual configuration” suggests.
Patchstack frames the honest version of this plainly: unauthenticated file inclusion happens every time the theme precondition is met, full code execution happens when the host environment also lines up. Enough hosts line up that the sensible default is to treat this as critical unless you’ve specifically checked your own stack and confirmed otherwise.
The Patch Does More Than Close the One Hole
WordPress shipped two changes in 7.1.2, not one. The first is the narrow fix: applying the same validate_file() check the sibling branch already had.
// wp-includes/template.php, WordPress 7.1.2
if ( $pagename_decoded !== $pagename && 0 === validate_file( $pagename_decoded ) ) {
$templates[] = "page-{$pagename_decoded}.php";
}
The second change is broader, and it’s the more telling one. 7.1.2 also introduces a containment check that every resolved template path now has to pass, regardless of which code branch produced it:
// wp-includes/template.php, new in WordPress 7.1.2
function _wp_is_template_path_allowed( $path ) {
global $wp_stylesheet_path, $wp_template_path;
// A file path that exists and does not contain `..` is allowed.
if ( 0 === preg_match( '#(?:^|/)\.\.[. ]*(?:/|$)#', wp_normalize_path( $path ) ) ) {
return true;
}
// Otherwise resolve the real path and require it to sit inside an allowed theme directory.
$real_path = realpath( $path );
// ...
}
A one-line fix to the specific missing validate_file() call would have closed the reported bug. Adding a second, path-agnostic containment check on top of it signals that WordPress’s security team didn’t want to rely on catching every future asymmetry between sibling code branches by hand. Instead, any template path that resolves outside an allowed theme directory now gets rejected structurally, whichever function produced it.
A CVSS Score That Depends on Who’s Scoring It
The two organizations that scored this bug landed in different places. Patchstack’s own CVSS 4.0 vector rates it AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N, a base score of 9.2, with attack complexity marked low but attack requirements marked “present,” a CVSS 4.0-specific category for extra preconditions like the theme-and-gadget-file requirements above. The National Vulnerability Database‘s independent scoring, published the same day under the older CVSS 3.1 framework, comes out lower: AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H, a base score of 8.1. CVSS 3.1 has no separate slot for “attack requirements,” so the same real-world preconditions that CVSS 4.0 tracks explicitly end up folded into a high attack-complexity rating instead. Both frameworks still land on the same practical read: unauthenticated, network-reachable, and a full loss of confidentiality, integrity, and availability if it lands. WordPress’s own release notes independently describe it as a critical severity vulnerability without attaching a number to it.
Every Supported Branch Back to 2016 Gets the Fix
The vulnerable code has existed since WordPress 4.7.0, released in December 2016, through 7.1.1. That’s every WordPress Core release for close to a decade. Because WordPress still backports security fixes to every branch it supports, the GitHub Security Advisory lists a full column of patched point releases running from 4.7.37 up through the current 7.1.2, covering 4.8.32, 4.9.33, 5.0.29, and every minor version in between. An older site doesn’t need to jump to the latest major version to get the fix; the backport for its own branch will do it. WordPress’s own announcement notes that the courtesy backports to the oldest branches were still shipping as they become ready at the time of the release.
WordPress Core’s Third Critical Release in Two Months
This is the second security-only WordPress Core release in under a week and the third critical or high-severity Core patch since mid-July. WordPress 7.1.1 shipped four days earlier, on September 18, closing an unauthenticated comment cross-site scripting bug alongside the Click2Shell remote code execution chain that sxz.io covered at the time. Before that, 7.0.2 in July closed a separate critical pre-authentication RCE chain after a public proof-of-concept surfaced. None of the three share a root cause, but the pattern is the same each time: a small, structural mistake somewhere in Core that needed no account to trigger.
As of this release, there’s no public evidence of active exploitation. NVD’s own risk-assessment data for CVE-2026-87902 marks exploitation status as “none” as of publication. That’s a reason to move quickly, not a reason to wait: the bug requires no attacker account, and Patchstack deliberately withheld a working request precisely because comparing the patched and unpatched code side by side is enough for a capable attacker to reconstruct one.
What to Do
Update to WordPress 7.1.2, or to the patched point release for your branch, from the WordPress dashboard under Updates, or by downloading it directly from wordpress.org. Sites with automatic background updates enabled should already have received it. If you’re running an older default theme like Twenty Twelve or Twenty Fourteen, or a theme that ships its own page- prefixed template directory, treat the update as higher priority rather than lower: that’s exactly the precondition this bug needs.








No Comment! Be the first one.