Like a hotel that checks your room key at the elevator but leaves the stairwell door propped open with a fire extinguisher
CVE-2026-87902 is a path-traversal-to-local-file-inclusion vulnerability in WordPress Core's page-template resolver (get_page_template() in wp-includes/template.php). When WordPress decides which template to render for a page, it builds a candidate filename from the pagename query parameter — page-{pagename}.php — and passes it to locate_template(). A neighboring code path applies validate_file() to block traversal, but the pagename branch does not. An attacker sends a double-URL-encoded traversal sequence (%252e%252e) that survives WordPress's slug sanitizer intact, then gets decoded by a late urldecode() call into ../, escaping the theme directory. Any readable .php file on the server can be included. If the server also has PEAR's pearcmd.php accessible and register_argc_argv=On in the PHP runtime, the attacker chains two requests to write and then execute a webshell — full unauthenticated RCE. Every WordPress version from 4.7.0 through 7.1.1 (a decade of releases across 23 branches) is affected. Fixed in 7.1.2 with backports to 4.7.37.
The GHSA advisory rates this CVSS 4.0: 9.2 (Critical) while a CVSS 3.1 evaluation produces 8.1 (High) — the gap is framework math, not disagreement. The CVSS 4.0 AT:P metric correctly signals that preconditions exist (the active theme must have a page-* directory; RCE further requires pearcmd.php and register_argc_argv), but the 9.2 top-line number overweights the worst-case RCE scenario that only materializes on a subset of deployments. The CVSS 3.1 AC:H captures this conditionality more faithfully. The LFI component alone — which needs only a qualifying theme — is reliably exploitable and serious (it can include core files, leak server-side behavior, and serve as a recon oracle), but it is not arbitrary code execution on every host. HIGH at 8.1 is the honest read: dangerous enough to demand immediate action, conditional enough that not every WordPress site is equally at risk.
5 steps from start to impact.
Fingerprint WordPress version and theme
wp-login.php, /wp-json/ REST endpoint). Version disclosure is on by default. The attacker also determines whether the active theme ships a page-* top-level directory — Twenty Twelve, Twenty Fourteen, Neve, Hestia, and Sydney all qualify. A simple request to /?page_id=2 confirms a published page exists.- Internet access to the target
- Target runs WordPress 4.7.0–7.1.1
- Version detection can be suppressed (rare in practice)
- Theme identification may require additional probing
Send double-encoded traversal payload
page_id=<valid_id>&pagename=templates%252F%252E%252E%252F%252E%252E%252F%252E%252E%252F<target>. The %252e sequences survive WordPress's slug sanitizer (which strips literal dots but preserves percent-encoded octets). The late urldecode() in get_page_template() converts them to ../, producing page-templates/../../../<target>.php. Multiple PoCs are public: abraxas/CVE-2026-87902, pwnVader/CVE-2026-87902-PoC-pwnVader, ressl/cve-2026-87902-poc, and vulpecuna/CVE-2026-87902.- Published page with known page_id
- Active theme has a top-level page-* directory
- Theme must contain page-* directory — not universal across all themes
- Some WAFs may detect double-encoded traversal sequences post-decode
%252e%252e in the pagename parameter. Nuclei template from griisemine/cve-2026-87902-detection performs behavioral comparison across three requests to confirm inclusion.Validate LFI with a benign oracle file
wp-links-opml.php (returns OPML XML), wp-cron.php (returns empty 200), or wp-content/index.php (returns empty body — the stock 'Silence is golden' file). If the response differs from the normal page template output, the LFI is confirmed. This is exactly what Patchstack observed in the wild — the Sep 22 probes targeted benign validation oracles, not weaponized payloads.- Target file exists and is readable by the PHP process
- Step 2 payload successfully escapes the theme directory
- File must exist at the expected traversal depth (attackers try 3–7 levels)
pagename parameter values containing encoded dots or slashes.Include pearcmd.php to write a webshell
pearcmd.php (commonly at /usr/share/php/pearcmd.php on Debian/Ubuntu or /usr/local/lib/php/pearcmd.php in Docker PHP images). When register_argc_argv=On, pearcmd.php reads $_SERVER['argv'] from the query string and interprets it as PEAR CLI commands. The attacker issues a config-create command that writes a PHP payload to a web-accessible directory (e.g., /var/www/html/shell.php). This is the well-known pearcmd-to-RCE technique documented by Orange Tsai and others.- pearcmd.php exists and is readable
- register_argc_argv=On in PHP runtime
- Web-writable directory exists
- pearcmd.php is present by default in official PHP Docker images and cPanel but absent on many hardened/managed hosts
- register_argc_argv is On by default in Docker PHP images and cPanel pre-8.5, but Off in most production php.ini configs
- Managed WordPress hosts (WP Engine, Kinsta, Pantheon) typically strip PEAR entirely
Execute the webshell for arbitrary code execution
www-data (or equivalent) service account. From here, the attacker can read wp-config.php for database credentials, pivot laterally, install persistent backdoors, or exfiltrate data. Note: this does NOT yield root — the blast radius is bounded by the web server's Unix permissions.- Step 4 succeeded — webshell was written
- Webshell path is accessible via LFI or direct HTTP
- SELinux/AppArmor policies may prevent PHP from writing outside designated directories
- Read-only container filesystems block the write entirely
- EDR/AV on the host may flag the webshell
The supporting signals.
| In-the-Wild Status | Active reconnaissance probing confirmed. Patchstack observed the first probes at 17:44 UTC on Sep 22, 2026 — less than 5 hours after 7.1.2 was published. Payloads match the diff exactly (someone was working from the patch, not an independent discovery). Traffic from IPs 169.58.48.193, 169.58.48.195, and 2001:df1:e8c0::106b using Go-http-client/1.1. Currently reconnaissance/fingerprinting stage; full RCE delivery not yet observed in telemetry. |
|---|---|
| Proof-of-Concept | Multiple public PoCs available. abraxas/CVE-2026-87902, pwnVader/CVE-2026-87902-PoC, ressl/cve-2026-87902-poc (pinned vulnerable lab), vulpecuna/CVE-2026-87902. Working PoC confirmed by Hadrian. Sploitus indexes at least two weaponized exploits. |
| EPSS Score | Not yet scored — CVE disclosed <24 hours ago (Sep 22, 2026). Given public PoCs, active probing, and WordPress's install base, expect rapid escalation to the 90th+ percentile once FIRST computes the initial score. |
| CISA KEV Status | Not listed as of Sep 23, 2026. Probing activity is reconnaissance, not confirmed exploitation with impact. KEV addition is plausible if exploitation escalates to webshell delivery. |
| CVSS Vector | CVSS 4.0: 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 → 9.2 Critical (from GHSA). CVSS 3.1: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H → 8.1 High. The AT:P (CVSS 4.0) and AC:H (CVSS 3.1) both reflect the theme and server preconditions. Network-accessible, no auth, no user interaction. |
| Affected Versions | WordPress 4.7.0 through 7.1.1 — 23 version branches spanning a decade of releases. The LFI precondition (theme with page-* directory) is met by default themes Twenty Twelve and Twenty Fourteen, and popular third-party themes Neve, Hestia, and Sydney. The RCE precondition (pearcmd.php + register_argc_argv=On) is met by default in official PHP Docker images and cPanel environments on PHP below 8.5. |
| Fixed Versions | 7.1.2, 7.0.6, 6.9.9, 6.8.10, and backports through 4.7.37 (all 23 supported branches). The fix adds validate_file() to the pagename branch and _wp_is_template_path_allowed() using realpath() containment in locate_template(). |
| Attack Surface / Exposure | WordPress powers ~33–43% of websites (~37.3 million active sites per BuiltWith 2026). An estimated 88% of WordPress sites run outdated releases (WPScan). Even if only 30–40% of sites have a qualifying theme, the exploitable population is 10M+ hosts. Internet-facing by design — no Shodan/GreyNoise-specific data yet for this CVE, but WordPress exposure is essentially the public web. |
| Disclosure Timeline | Jul 20, 2026 — private report via HackerOne by Robert Ressl. Jul 21, 2026 — receipt acknowledged. Sep 15, 2026 — WordPress team requested attribution details. Sep 22, 2026 — public advisory and 7.1.2 release. Sep 22, 2026 17:44 UTC — first probing observed in the wild. |
| Reporting Researcher | Robert Ressl (ressl.ch). Responsible disclosure via HackerOne with 64-day coordinated timeline. All exploitation testing performed in isolated local labs using WordPress 7.0.2. |
Why this verdict
- Unauthenticated, zero-click, network-reachable: No credentials, no user interaction — any internet-facing WordPress site in the affected version range is in the blast radius for the LFI component. This establishes a HIGH baseline before friction analysis.
- Theme precondition narrows but does not eliminate the population: The
page-*directory requirement is met by default themes (Twenty Twelve, Twenty Fourteen) and popular third-party themes (Neve, Hestia, Sydney), covering an estimated 30–40% of WordPress installations. This is a meaningful filter but still represents 10M+ exploitable hosts — not enough friction to pull below HIGH. - RCE is conditional, not universal: Full code execution requires
pearcmd.phppresence ANDregister_argc_argv=On— common in Docker-based and cPanel deployments but not the majority of production WordPress hosts. LFI alone is serious (includes arbitrary PHP files, serves as reconnaissance oracle, can chain with other server-side files) but is not equivalent to arbitrary command execution. This is the primary downward pressure from CRITICAL to HIGH. - Role multiplier: WordPress is a web CMS deployed on web/application servers. Compromise yields
www-datashell at best — host-scoped, not domain/fleet/supply-chain scale. WordPress is *not* canonically in the high-value infrastructure catalog (not an IdP, hypervisor, PAM vault, backup system, CI/CD pipeline, or network edge appliance). The blast radius per-host is significant (database credentials viawp-config.php, persistent webshell, lateral movement springboard) but does not inherently cascade to domain takeover or fleet compromise. The high-value-role floor does not override: WordPress's canonical deployment role is line-of-business web presence, not identity or infrastructure control plane. No floor elevation applies. - Active probing and PoC availability compress the exploitation timeline: Multiple public PoCs and observed probing within 5 hours of patch release mean this is an imminent threat, not a theoretical one. This does not change the severity score (which measures impact and exploitability, not urgency) but triggers the noisgate exploitation override for mitigation deadlines.
Why not higher?
RCE requires multiple compounding preconditions beyond the base LFI — pearcmd.php must be present, register_argc_argv must be enabled, and a writable directory must exist. Each condition narrows the exploitable population. WordPress is a web CMS, not identity infrastructure, a hypervisor, or a PAM vault; compromise yields www-data privileges on a single host, not domain admin or fleet control. The CVSS 4.0 AT:P metric correctly reflects this conditionality — the 9.2 score overweights the RCE scenario that only materializes on a subset of deployments.
Why not lower?
The LFI component requires NO additional preconditions beyond a common theme layout — an attacker can reliably include arbitrary PHP files from any qualifying site without authentication. WordPress powers 37 million active sites; even a 30% exploitable fraction is 11 million hosts. Active probing within hours of disclosure, with multiple public weaponized PoCs, makes exploitation imminent rather than theoretical. Dropping below HIGH would be irresponsible given the unauthenticated attack surface and installed base.
What to do — in priority order.
- Deploy WAF rule blocking double-encoded traversal in pagename — Add a WAF rule (ModSecurity, Cloudflare, AWS WAF) that rejects requests where the
pagenamequery parameter (GET or POST) contains%252eor%252fafter URL decoding. This is the most targeted compensating control and does not affect legitimate traffic — no valid WordPress page slug contains percent-encoded dots. Given active probing and the noisgate exploitation override, deploy within hours, not 30 days. Test against your staging environment first to confirm no false positives on slug-based routing. - Switch to a theme without a page-* directory — If patching is delayed, activate a theme that does not contain a top-level
page-*directory (e.g., Twenty Twenty-One through Twenty Twenty-Six use different layouts). This eliminates the LFI precondition entirely. Verify by listingls -d /path/to/themes/*/page-*— if nothing returns, the precondition is not met. Deploy within the noisgate exploitation override window (hours). - Remove or restrict pearcmd.php — Delete
/usr/share/php/pearcmd.php(or equivalent path) or make it unreadable by the web server user. This collapses the RCE chain entirely, even if the LFI succeeds. On Docker PHP images:rm /usr/local/lib/php/pearcmd.php. On cPanel:chmod 000 /usr/share/pear/pearcmd.php. Deploy within hours on internet-facing hosts. - Disable register_argc_argv in php.ini — Set
register_argc_argv = Offin the PHP configuration used by the web server (not just the CLI php.ini). Restart PHP-FPM or Apache. This preventspearcmd.phpfrom reading attacker-controlled arguments even if included. Verify withphp -i | grep register_argc_argvfrom the web server context. Deploy within hours on internet-facing hosts. - Monitor access logs for traversal indicators — Set up real-time log alerting for requests where the
pagenameparameter contains%252e,%252f,.., or references topearcmd,wp-links-opml, orwp-cronin unexpected contexts. Patchstack documented specific IoC patterns: User-AgentGo-http-client/1.1, IPs169.58.48.193and169.58.48.195. Alert on these immediately. - Patch to WordPress 7.1.2 or branch backport — This is the definitive fix. Update via the WordPress dashboard (auto-update if enabled) or manually apply the branch-specific backport (7.0.6, 6.9.9, 6.8.10, down to 4.7.37). The patch adds
validate_file()to the pagename branch and introduces_wp_is_template_path_allowed()withrealpath()containment. Under the noisgate remediation SLA for HIGH, the deadline is 180 days, but given active probing, prioritize internet-facing instances this week.
- Rate limiting — the exploit is a single GET request per target. Rate limiting will not prevent a one-shot LFI/RCE chain. The attacker needs exactly two requests for the full RCE path (one to write the webshell, one to execute it).
- Disabling XML-RPC or REST API — the attack vector is the standard front-end page request path (
/?page_id=X&pagename=Y), not XML-RPC or the REST API. Disabling these endpoints provides no protection against this CVE. - WordPress security plugins that only inspect POST bodies — the primary attack works via GET query string parameters. Plugins like Wordfence or Sucuri may detect the traversal *if* they inspect URL parameters post-decode, but plugins that only hook into
wp_loadedor later may fire after the template resolution has already occurred. Do not rely on plugin-level WAF alone — use a network-layer WAF upstream.
Crowdsourced verification payload.
Run this script on the WordPress host (or any machine with read access to the webroot via mount/SSH). Invoke with bash cve-2026-87902-check.sh /var/www/html (adjust the path to your WordPress document root). No elevated privileges required — only read access to wp-includes/. The script also checks RCE preconditions (pearcmd.php, register_argc_argv) which may require broader read access.
#!/usr/bin/env bash
# CVE-2026-87902 — WordPress Core LFI via page-template resolution
# Usage: bash cve-2026-87902-check.sh /var/www/html
# Exit: 0=PATCHED 1=VULNERABLE 2=UNKNOWN
set -uo pipefail
WP="${1:?Usage: $0 /path/to/wordpress}"
TMPL="$WP/wp-includes/template.php"
VERF="$WP/wp-includes/version.php"
if [ ! -f "$VERF" ]; then
echo "UNKNOWN - $VERF not found. Is this a WordPress webroot?"
exit 2
fi
wp_ver=$(awk -F"'" '/wp_version/{print $2;exit}' "$VERF")
echo "[*] WordPress version: $wp_ver"
if [ ! -f "$TMPL" ]; then
echo "UNKNOWN - $TMPL not found."
exit 2
fi
# Primary check: does template.php contain the fix?
if grep -q '_wp_is_template_path_allowed' "$TMPL" 2>/dev/null; then
echo "PATCHED - template.php contains the CVE-2026-87902 fix (_wp_is_template_path_allowed)."
exit 0
fi
echo "VULNERABLE - template.php lacks the CVE-2026-87902 fix (v$wp_ver)."
echo ""
echo "--- Precondition checks ---"
# Check 1: Theme has page-* directory (LFI precondition)
THEMES="$WP/wp-content/themes"
if [ -d "$THEMES" ]; then
hits=$(find "$THEMES" -maxdepth 2 -type d -name 'page-*' 2>/dev/null)
if [ -n "$hits" ]; then
echo " WARN: page-* directory found in themes - LFI precondition MET"
echo "$hits" | sed 's/^/ /'
else
echo " INFO: No page-* directory in themes - LFI precondition not met (lower risk)"
fi
fi
# Check 2: pearcmd.php exists (RCE precondition)
pear=$(find / -name pearcmd.php -readable 2>/dev/null | head -1)
if [ -n "$pear" ]; then
echo " WARN: pearcmd.php found at $pear - RCE escalation path exists"
else
echo " INFO: pearcmd.php not found - RCE chain unlikely"
fi
# Check 3: register_argc_argv enabled (RCE precondition)
if command -v php >/dev/null 2>&1; then
argc=$(php -r 'echo ini_get("register_argc_argv");' 2>/dev/null || true)
if [ "$argc" = "1" ]; then
echo " WARN: register_argc_argv is ON - RCE precondition met"
else
echo " INFO: register_argc_argv is OFF or unset"
fi
else
echo " INFO: php binary not in PATH - cannot check register_argc_argv"
fi
exit 1- GHSA Advisory — WordPress/wordpress-develop
- Patchstack — Attackers Started Probing Hours After the Patch
- Robert Ressl — Researcher Writeup
- Hadrian — Working PoC Analysis
- WordPress 7.1.2 Release Announcement
- SecurityOnline — Critical WordPress RCE Flaw
- griisemine — Nuclei Detection Template
- abraxas — Public PoC Repository
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.