- **Fixed:** 7.1.2 and per-branch backports (4.7.37, 5.9.18, 6.9.9, 7.0.6, …) - 22 Sep 2026
- **CVSS 4.0:** 9.2 (`AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N`)
- **CWE-98** - Improper Control of Filename for Include/Require Statement
WordPress 7.1.1 (Twenty Twelve theme) permits an unauthenticated attacker to
include PHP files from outside the theme directory via the `pagename` query
variable, and - with the stock `register_argc_argv = On` of the official PHP
image - to write and execute a marker file via PEAR `pearcmd.php`:
Theme: twentytwelve (page-templates/ present)
PHP: 8.2 (official image, register_argc_argv=On, PEAR present)
Result: LFI oracle fired (); marker probe executed as
WordPress 7.1.2 with the identical setup blocks the oracle entirely
(negative control). This demonstrates code execution as the WordPress
web-server user inside the container. It does not demonstrate root access,
container escape, lateral movement, or host compromise.
Run this repository only on systems you own or are explicitly authorized to
test. The lab driver refuses variants other than `vulnerable`/`patched`, the
Python drivers refuse non-loopback targets unless
`--allow-authorized-non-loopback` is supplied, and the Compose port is bound
See [SECURITY.md](SECURITY.md) for disclosure handling. This repository does
not include production targets, customer data, access tokens, or evidence
- Docker publishes WordPress on `127.0.0.1` only.
- The default PoC mode is a **harmless LFI oracle**: it includes
`wp-links-opml.php`, a stock core file that prints an OPML document.
- `--rce` is opt-in and writes a **quoteless, self-deleting** marker probe
(``) via the publicly documented pearcmd.php
`config-create` technique; the probe removes itself on its single execution
and the PoC verifies deletion. No shells, callbacks, or persistence.
- The lab re-provisions from pristine official images on every `start`; no
- Requests are single-host, sequential, and rate-limited. No mass scanning.
Requirements: Docker with Compose v2, Python 3.10+, network access to
Docker Hub and wordpress.org (theme install).
This builds the vulnerable lab, requires the LFI oracle and the RCE marker to
fire, then builds the patched lab and requires the oracle to stay silent.
To use another loopback port: `LAB_PORT=9499 ./lab verify`.
Remove everything afterwards with `./lab reset`.
[+] page-* folder(s) reachable: page-templates (traversal can resolve)
[!!!] LFI CONFIRMED (safe oracle: wp-links-opml.php)
[RCE] pearcmd.php reachable and taking argv: /usr/local/lib/php/pearcmd.php
[!!!] RCE CONFIRMED (pearcmd.php config-create file write):
evidence: uid=33(www-data) (command `id` ran as the web user)
cleanup : probe self-deleted on execution: confirmed
[=] oracles did not fire: target patched (>= 7.1.2), ...
An HTTP 200, a WAF block, or a partial traversal without the oracle marker is
./lab start vulnerable # fresh 7.1.1 lab on 127.0.0.1:9471
./lab test # assert LFI + RCE
./lab start patched # fresh 7.1.2 lab
./lab test # assert oracle absence
./lab reset # containers, volumes, state
Run the tooling by hand against the running lab:
python3 poc/triage_cve_2026_87902.py --url
python3 poc/poc_cve_2026_87902.py --url --rce
nuclei -t nuclei/cve-2026-87902.yaml -u -silent
For systems you own or are explicitly authorized to test (bug-bounty scope,
written engagement), the drivers require the explicit override:
python3 poc/poc_cve_2026_87902.py --url \
--allow-authorized-non-loopback [--insecure] [--rce]
`get_page_template()` in `wp-includes/template.php` builds a candidate template
filename from the `pagename` query variable. The sibling branch three lines
above passes the page-template slug through `validate_file()`; the `pagename`
branch never did, and additionally pushes an extra `urldecode()`-decoded
// wp-includes/template.php, get_page_template(), WordPress <= 7.1.1
$pagename_decoded = urldecode( $pagename );
if ( $pagename_decoded !== $pagename ) { // 7.1.2 adds validate_file()
$templates[] = "page-{$pagename_decoded}.php";
7.1.2 applies `validate_file()` to the decoded candidate **and** adds a
containment gate (`_wp_is_template_path_allowed()`) that every resolved
1. The active theme (child **or** parent - `locate_template()` checks both)
ships a top-level directory starting with `page-`. Legacy default themes
(Twenty Twelve, Twenty Fourteen) and several wp.org themes (Neve, Hestia,
Sydney) qualify. Researched commercial themes (Divi, Avada, Flatsome,
Woodmart, Betheme, The7, Enfold, Salient, Jupiter/X, Porto, Houzez, XStore,
Kalium, Uncode) ship **none** - the traversal cannot resolve; see
`PRO_THEME_PAGE_DIRS` in the PoC for the mirror-sourced list.
2. The included file must end in `.php` (appended by the code).
3. RCE additionally needs `register_argc_argv = On` and PEAR's `pearcmd.php`.
## Lab-verified operational notes (beyond the public advisories)
The advisory and the Patchstack writeup decline to publish a working request.
These details were established empirically in this lab:
1. **A `page_id` anchor is mandatory.** An unresolved `pagename` flips
`is_404`, whose template shadows `get_page_template()` entirely. Anchor the
query with any resolvable published `page_id`.
2. **Full double-encoding is required.** `get_posts()` rewrites `pagename`
through `sanitize_title_for_query( wp_basename( … ) )`: raw `..` becomes
`-`, but `%hex` sequences survive; the extra `urldecode()` in
`get_page_template()` reveals the traversal. Only `%252e`/`%252f` works.
3. **Pretty permalinks force POST** (`redirect_canonical` 301s GET and drops
the payload; it returns immediately for non-GET/HEAD).
4. **Exact depth:** four `../` from `page-templates/` on a standard docroot.
5. **The payload fires from the POST body too** (WP merges `$_POST` into query
vars) - the basis of the WAF-evasion tiers.
6. **pearcmd transport quirks:** PHP does not urldecode `$argv` tokens, and
PEAR's config serializer escapes both quote styles - hence the raw,
| `poc/poc_cve_2026_87902.py` | Full PoC: version + WAF fingerprint, theme/`page-*` folder enumeration (wp.org SVN, asset-reference discovery, researched pro-theme map), anchor discovery, 8-variant LFI oracle, comparative empty-body fallback oracle, `--rce` pearcmd chain, `--lab` docker demo |
| `poc/triage_cve_2026_87902.py` | Per-target or batch verdict with the named failing gate (EXPLOITABLE / NOT EXPLOITABLE / WAF SUSPECTED / INCONCLUSIVE) |
| `poc/bounty_screen.py` | Works an **in-scope** bug-bounty asset list: crt.sh wildcard expansion → WP filter → precondition screen → candidate handoff |
| `nuclei/cve-2026-87902.yaml` | Nuclei template: safe LFI oracle (174 payloads: 10 `page-*` remainders, double/triple-encoded tiers, ids 1–30) |
WAF fingerprinting covers Imperva/Incapsula, Cloudflare, Akamai, AWS
CloudFront/WAF, Sucuri, F5. Eight oracle variants: double-encoded, alternate
`%25%32%65` spelling, parameter pollution, triple-encoded - each in query
string and POST body. Multipart bodies and raw-slash payloads were tested and
rejected (WP does not process them for this sink).
| WordPress 7.1.1 | LFI + pearcmd RCE reproduced end to end |
| WordPress 6.5.3 / 5.9.17 | LFI oracle reproduced |
| WordPress 4.7.0–7.1.1 (other branches) | Reported affected; branch table per GHSA |
| WordPress 7.1.2 | Patched negative control reproduced |
Not committed: `.lab-state`, `__pycache__/`, screener output files.
| Unauthenticated LFI outside the theme directory | Proven (7.1.1, 6.5.3, 5.9.17) |
| RCE via pearcmd.php `config-create` as the web user | Proven (7.1.1 lab) |
| Probe self-deletion / no persistent artifacts | Proven |
| Patched negative control (7.1.2) | Proven |
| Every intermediate affected release tested | Not tested |
| WAF bypass variants against live commercial WAFs | Lab-derived, not WAF-tested |
| Root privilege / container escape / host compromise | Not claimed |
- [GHSA-7hp8-65ch-5whp](
- [Patchstack: WordPress 7.1.2 security release - unauthenticated LFI to RCE](
- [Hadrian: CVE-2026-87902 working PoC - independent reconstruction](
(confirms the `page_id` anchor and double-encoding requirements; source of
the comparative `wp-content/index.php` empty-body oracle, included here as a
- [WordPress 7.1.2 release announcement](
- [NVD entry](
Vulnerability reported by Robert Ressl. This repository is independent
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
