WordPress 7.1.2 landed on 22 September 2026. It’s a security-only release with a single fix, and that fix is the most serious thing WordPress has patched in a while: an unauthenticated local file inclusion in page template resolution that can reach remote code execution.
Patchstack customers are protected by a RapidMitigate rule. We still recommend updating to the most recent version of WordPress available.
✌️ Our users are protected from this vulnerability. Are yours?
Identify vulnerabilities in your plugins and get recommendations for fixes.
Request auditProtect your users, improve server health and earn additional revenue.
Patchstack for hostsUnauthenticated Local File Inclusion in Page Template Resolution
The vulnerability was reported by Robert Ressl and affects WordPress Core from 4.7.0 up to and including 7.1.1. It carries a CVSS 4.0 score of 9.2 (AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N) and is classified as CWE-98, improper control of filename for an include statement. CVE-2026-87902 has been assigned to it.
Every branch back to 4.7 is affected, which is close to a decade of releases, and no account is needed to attack it. That combination is what makes this one worth acting on today rather than at the next maintenance window.
The Sink
When WordPress renders a page, get_page_template() in wp-includes/template.php builds a list of candidate template filenames and hands them to the template loader. One of those candidates is built from the pagename query variable, which comes straight from the request:
// 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 next to each other. The page template slug on the first line is passed through validate_file(), WordPress’s own traversal check, before it’s accepted. The candidate built from pagename never was. The protection existed three lines above the place it was missing.
Two details decide what an attacker can actually reach. The filename is assembled as "page-{$pagename}.php", so the payload has to continue a directory that genuinely starts with page-, and the target has to end in .php because the extension is appended. That’s why the practical precondition is an active theme carrying a top-level directory like page-templates, which legacy default themes and a number of popular third-party themes do.
The extra urldecode() call is what turns a traversal-shaped slug into a real path. We aren’t publishing a working request.
What It Takes to Reach Code Execution
Including a local .php file is not by itself code execution of the attacker’s choosing. It runs whatever that file does. Getting from there to arbitrary code needs a readable .php file on the server that behaves usefully when included, and the well-known candidate is PEAR’s pearcmd.php, which only becomes useful 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 on PHP below 8.5, so “unusual configuration” undersells how common it is.
So the honest framing is a conditional chain: unauthenticated file inclusion always, code execution when the host happens to line up. Plenty of hosts line up. Treat it as critical unless you have checked your own stack and know otherwise.
The Fix
WordPress shipped two changes. The first closes this specific hole by 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 is the more interesting one. 7.1.2 also introduces a containment check that every resolved template now has to pass, regardless of which code path 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 );
// ...
}
That second change says something the advisory doesn’t spell out. A one-line validation fix would have been enough for the reported issue. Adding a check that every template path must resolve inside the stylesheet directory, the template directory, or theme-compat means the security team treated template resolution as a class of problem rather than a single bug. It’s the right call, and it suggests they weren’t confident the reported path was the only one.
Timeline
Update Now
WordPress 7.1.2 is available from the Dashboard under Updates, or from WordPress.org directly. Sites with automatic background updates enabled should already have it. Because this one reaches back to 4.7, WordPress has published fixed releases for every branch it still backports to, so an older site can take the fix without a major version jump. Only the most recent version of WordPress is actively supported, and that remains the version to be on.
If you can’t update immediately, two things reduce your exposure in the meantime: check whether your active theme has a top-level directory beginning with page-, and check whether your PHP has register_argc_argv enabled. Neither is a fix, but both tell you how close to the worst case you are.


