BLOG

CVE-2026-87902: Critical WordPress file inclusion and conditional RCE

Published 9 min read

An unauthenticated page-template traversal in WordPress Core can lead to local PHP file inclusion and conditional RCE. The root cause, deployment prerequisites, laboratory evidence and disclosure timeline behind my finding.

On September 22, 2026, WordPress published a fix for a critical vulnerability I discovered and reported privately through HackerOne. CVE-2026-87902 allows an unauthenticated attacker to make page-template resolution include a readable local PHP file outside the active theme directories. Under specific theme and server conditions, that inclusion can be turned into remote code execution.

The official WordPress advisory (opens in a new tab) assigns 9.2/10 under CVSS 4.0 and lists WordPress 7.1.2 as the fixed release for the 7.1 branch, alongside backports to older branches down to 4.7. I have published a proof of concept and reproducible local lab (opens in a new tab) documenting the conditions, results and limitations.

Robert Ressl beside a fractured WordPress emblem. CVE-2026-87902 is rated Critical, CVSS 4.0 9.2/10, with unauthenticated access, conditional RCE and a fix in WordPress 7.1.2.

What the finding means

The core vulnerability is local PHP file inclusion through an unauthenticated frontend request. My demonstrated route from inclusion to arbitrary PHP execution uses PEAR components already present in the tested server environment. PEAR is not a WordPress dependency, and that route is not available on every deployment.

Question Confirmed result
Is a WordPress account required? No account, cookie, session or nonce was needed in the demonstrated chain.
Is a plugin or user interaction required? Neither was required.
What was dynamically tested? WordPress 7.0.2 in isolated local laboratories.
What privileges does execution obtain? Those of the PHP/web-server account, www-data in the labs.
Is every default installation exploitable? The evidence does not establish that. Theme layout and runtime prerequisites matter.
How common are the required conditions? I have not measured their prevalence across live sites.

These distinctions matter when interpreting the severity. The impact on a configuration that satisfies the prerequisites is serious, but the score is not a measurement of how many WordPress sites satisfy them.

How page-template resolution crosses the boundary

The failure spans request parsing, page selection and template loading. In the tested version, each stage accepts a value that becomes dangerous when interpreted by a later stage.

  1. Two public query variables arrive together. WordPress accepts pagename and page_id from an anonymous form POST. No authenticated administrative action is involved.
  2. Early sanitization preserves encoded characters. A double-encoded traversal reaches query processing with percent-encoded octets still present. The encoded separators are not treated as ordinary path separators at that point.
  3. Page selection and template naming diverge. A valid numeric page_id selects a real published Page after the supplied pathname fails to resolve. The supplied pagename remains in the query object.
  4. A later decode changes the meaning. get_page_template() applies urldecode() and constructs a candidate with a fixed page- prefix and .php suffix. The previously encoded separators and parent-directory components become active filesystem syntax.
  5. The final include is not confined to a theme root. Template lookup checks for an existing file. The final loader canonicalizes the path and checks its type, suffix and readability, but does not prove that the resolved path remains inside an allowed theme directory.

The important distinction is between canonicalization and containment. Resolving a path with realpath() gives a normalized destination. It does not establish that the destination belongs to a directory the application intended to trust.

The public repository (opens in a new tab) maps this data flow to the relevant locations in the tested WordPress 7.0.2 source.

Why the theme directory matters

WordPress supplies the fixed page- prefix. For the demonstrated traversal, the first filesystem component therefore needs to be an existing directory whose name begins with page-, directly inside the active theme root. A directory such as page-templates/ satisfies that part of the path resolution.

The directory can be in the active child theme or its parent theme. A directory symlink can also satisfy the filesystem condition. It must be traversable by the PHP process, but it does not have to be writable or contain files. My original proof used an empty, root-owned directory with mode 0755.

An ordinary template file such as page-about.php is not that directory. Equally, having the right directory alone does not establish a complete exploit chain.

Inclusion prerequisite Why it matters
A published, anonymously accessible Page selectable by page_id The request must resolve to a real Page.
No earlier valid assigned custom template An existing assigned template can take precedence over the affected candidate.
A suitable top-level page-* directory in a theme root searched by WordPress The prefixed path must resolve through that first component.
An existing readable local .php target WordPress appends the suffix and the loader checks the file.
Filesystem policy permits the include Confinement can prevent access to the chosen target.

The versions of Twenty Twenty-Three, Twenty Twenty-Four and Twenty Twenty-Five inspected during the original research lacked a top-level page-* directory. I deliberately added the empty fixture to Twenty Twenty-Five in the lab. This is an explicit test prerequisite, not evidence of an exploitable untouched default installation.

The layout itself is legitimate: WordPress's theme documentation (opens in a new tab) describes page-templates/ as a convention for organizing custom page templates. That documentation establishes a real use for the layout; it does not quantify its deployment or the prevalence of the complete RCE chain.

From inclusion to PHP execution

In my original tested environment, the official wordpress:php8.3-apache runtime included a readable PEAR command entry point and its dependencies. The web-facing PHP runtime had register_argc_argv=On, and the PHP account could write to a temporary directory.

The demonstration used two anonymous requests. The first included the local PEAR entry point and used its configuration-writing functionality to create a file containing controlled PHP. The second included that generated .php file through the same WordPress defect.

This particular route additionally depended on:

  • A readable pearcmd.php with the PEAR components it needs.
  • register_argc_argv=On in the PHP configuration actually used for web requests.
  • A writable destination for the generated file.
  • Request handling and filesystem policy that permit the demonstrated sequence.

The original runtime loaded no main php.ini, so the compiled setting applied. The published lab makes register_argc_argv=On explicit for reproducibility. Configuration of the web-facing runtime matters; inspecting only the command-line PHP configuration does not establish how the website runs.

Disabling that setting or removing PEAR breaks this demonstrated route. It does not repair WordPress's underlying file-inclusion flaw. Impact without PEAR depends on the other local PHP files and conditions available on that system.

What I verified in the laboratory

The original report records the complete chain in three fresh, isolated laboratories using WordPress 7.0.2. The generated file belonged to www-data, and the follow-up include executed PHP that read a laboratory proof artifact.

That artifact was root-owned but deliberately world-readable. Reading it demonstrates PHP execution and file access as the web-server account. It does not demonstrate operating-system privilege escalation or root access. No third-party or production WordPress site was tested.

The original controls included removing the theme-directory fixture, disabling register_argc_argv, removing PEAR and choosing a non-writable output directory. Each broke the demonstrated chain at the expected prerequisite. The public repository also records a fresh reproduction on September 22 and a control with the fixture removed.

The practical impact of arbitrary PHP execution is bounded by the service account's permissions. Depending on those permissions, an attacker could access configuration and database credentials, read or change site data, modify writable application resources, or disrupt the site. No host escape was needed or demonstrated.

The repository provides the lab setup, PoC and recorded results (opens in a new tab). Its lab service is bound to loopback. The reproduction evidence is specific to the documented configuration, and a failed attempt on another configuration is not proof that the underlying WordPress version is fixed.

Severity, fixes and operator action

The advisory's CVSS 4.0 vector is:

CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N

AT:P records that attack requirements are present. It captures the deployment and execution conditions that must line up for the demonstrated impact. The score of 9.2, Critical, and conditional exploitability describe different aspects of the same finding.

For site owners, the primary action is to install a patched WordPress release. The advisory lists 7.1.2 for the 7.1 branch and 7.0.6 for the 7.0 branch, with additional backports down to the 4.7 branch. Consult the complete affected and patched version list (opens in a new tab) for the appropriate release.

Additional hardening can reduce the available inclusion-to-execution paths: disable register_argc_argv for web requests when it is unnecessary, remove unused web-readable PEAR entry points, and restrict the PHP account's filesystem access and write permissions. These measures supplement the core update. They do not replace it.

The fix status and backport list in this article come from WordPress's advisory. My recorded exploitation tests were on WordPress 7.0.2; I did not independently test the released patch.

Disclosure timeline

Date Event
July 20, 2026, 23:46 UTC Submitted privately through the WordPress HackerOne program.
July 21, 2026, 09:49 UTC Receipt acknowledged.
September 15, 2026 Informed that a fix was planned for an upcoming release; the team requested my attribution details.
September 22, 2026 WordPress published the advisory and released the fix; I published the PoC repository.

The initial classification was revised and the report was accepted as a valid security finding. I was not given an exact release date in advance, and I had not requested one. Thank you to the WordPress security team for addressing the issue and crediting the research.

References

The research and exploitation described here were confined to self-created local laboratories. AI tools assisted with organizing and reviewing the write-up; I remain responsible for the technical claims. The header artwork is an AI-generated composition using my portrait.