Someone left the theme folder's name on the office door, and now every lockpick on earth knows which key opens the server room
CVE-2026-87902 is a Local File Inclusion flaw in WordPress Core's get_page_template() function affecting every release from 4.7.0 through 7.1.1 — nearly a decade of code. An unauthenticated attacker sends a double-URL-encoded path-traversal payload in the pagename query parameter. WordPress's first sanitisation pass sees harmless percent-encoded text; a subsequent urldecode() call converts it into live ../ sequences, and locate_template() happily includes any readable .php file on the server — outside the theme directory, no login required. When the active theme ships a top-level directory whose name starts with page- (Twenty Twelve, Twenty Fourteen, Neve, Hestia, Sydney, and others) and PHP's register_argc_argv is On and PEAR's pearcmd.php is present, the attacker chains a second request to write an arbitrary PHP webshell and achieve full remote code execution.
The vendor tagged this HIGH / 8.1 with AC:H to reflect the RCE preconditions. That rating under-counts the real-world risk. The LFI itself is unconditional on any unpatched WordPress — no theme check, no PHP flag, no special config. More importantly, the RCE conditions are met *by default* in official PHP Docker images and cPanel environments running PHP < 8.5, which together cover a large fraction of containerised and shared-hosting WordPress installs. Within hours of the September 22 patch, automated exploitation began; by day five, CrowdSec counted 30,813 unique attacking IPs and 322,680 exploitation signals, and TechTimes reported 350,000+ compromised sites. CISA added the CVE to KEV on September 25 with a three-day remediation deadline for FCEB agencies. The vendor's 8.1 is defensible in a vacuum, but the explosive real-world exploitation, commodity tooling, and WordPress's 43 % web-market share make this functionally CRITICAL for any organisation running unpatched WordPress.
5 steps from start to impact.
Reconnaissance — identify WordPress version
/feed/, /readme.html, or the <meta name="generator"> tag to confirm WordPress and its version. Shodan, FOFA, and Censys dorks surface millions of WordPress hosts. No authentication or interaction is required.- Target runs WordPress 4.7.0–7.1.1
- WordPress is reachable over HTTP/S
- Sites behind CDNs may strip version metadata, but the attack still works without fingerprinting — attackers spray blindly
CVE-2026-87902.yaml (projectdiscovery/nuclei-templates#17302) detects the vulnerable code path non-invasively.Craft double-encoded pagename traversal
pagename set to a double-URL-encoded traversal sequence (e.g., %252e%252e%252f repeated 1–12 levels) targeting a known .php file outside the theme directory. A valid page_id of a published Page is included so WordPress enters page-template resolution. Tools: exploit_locate_template_rce.py, CVE-2026-87902-Toolkit, or a raw curl one-liner. Uppercase hex and varying depths evade signature WAFs.- Unpatched WordPress 4.7.0–7.1.1
- At least one published Page exists (default on fresh installs)
- WAFs with deep-decode inspection (Cloudflare managed rule shipped Oct 1) block the most common encodings, but multi-encoding and multipart/form-data wrapping bypass simpler rulesets
pagename values with %2e or %252e sequences; Patchstack virtual-patch rule shipped Sep 22.LFI triggers — arbitrary .php included
get_page_template() calls urldecode() on the payload, producing real ../ components. locate_template() checks file existence and readability but never validates the resolved path stays inside the theme directory. The attacker-chosen .php file is loaded via include. This step succeeds on any unpatched WordPress regardless of theme or PHP config. By itself it yields information disclosure (e.g., including files that echo configuration) or execution of existing server-side PHP code.- Steps 1–2 succeeded
- The included file must be a
.phpfile (not arbitrary read of non-PHP files) - Useful LFI targets beyond pearcmd vary by hosting stack
page- followed by path traversal in the URI; unusual include() paths in PHP open_basedir violation logs.(Conditional) Include pearcmd.php with config-create
/usr/local/lib/php/pearcmd.php (or distro equivalent) exists, is readable, and PHP's register_argc_argv=On, the attacker appends +config-create+/&/<php payload>/+/tmp/shell.php to the query string. Because register_argc_argv passes the query string as $argv to included scripts, pearcmd interprets this as a config-create command and writes attacker-controlled PHP to disk. Observed webshell filenames: wp-pear-rce-flag.php, poc87902.php, luci_*, zeta_*.- Active theme has a
page-prefixed top-level directory - pearcmd.php exists and is readable
- register_argc_argv=On (default in official PHP Docker images, cPanel < PHP 8.5)
- Production
php.inicommonly sets register_argc_argv=Off - Managed WordPress hosts (WP Engine, Kinsta, Flywheel) strip PEAR entirely
- open_basedir restrictions may block traversal to pearcmd path
.php files in /tmp, /var/tmp, wp-content/uploads; Sysdig Falco or auditd rules for pearcmd.php being opened by httpd/php-fpm.(Conditional) Execute webshell — full RCE
/tmp/shell.php. The attacker now has arbitrary PHP execution as the web-server user (typically www-data). From here the standard post-exploitation playbook applies: database credential theft from wp-config.php, lateral movement, crypto-mining, or ransomware staging.- Step 4 succeeded
- Web server user can read the written file
/tmpmay be mountednoexecon hardened hosts (but include bypass noexec)- SELinux/AppArmor
httpd_tconfinement may block reads from/tmpon RHEL/derivatives
system(), passthru(), eval(base64_decode( patterns).wp core update via WP-CLI or apply the one-click update from the dashboard. Confirm with wp core version.register_argc_argv=Off in php.ini (or PHP-FPM pool config) and restart PHP. This breaks the demonstrated pearcmd RCE chain completely — pearcmd can no longer receive query-string arguments as $argv. The LFI still works, but the most dangerous escalation path is severed. Deploy within hours as a stop-gap if patching WordPress requires a maintenance window. Verify with php -i | grep register_argc_argv.pagename parameters containing %2e, %252e, or .. after recursive URL-decoding. This blocks the exploitation vector at the network edge. Deploy within hours. Note: determined attackers may find encoding bypasses, so this is defense-in-depth, not a substitute for patching./usr/local/lib/php/pearcmd.php and peclcmd.php, or chmod 000 them. This eliminates the known RCE gadget. The LFI persists, but the proven code-execution path is gone. On Docker images, add RUN rm -f /usr/local/lib/php/pearcmd.php /usr/local/lib/php/peclcmd.php to your Dockerfile.page-templates/ or similar page-* directory at its root, switching to a theme that lacks this structure (e.g., Twenty Twenty-One, Twenty Twenty-Three, Flavor) breaks the template-resolution traversal chain. This is a tactical stop-gap only — it addresses the RCE path but the LFI may still be exploitable through other filesystem paths.open_basedir in PHP to restrict file access to the WordPress docroot and essential PHP directories. This prevents include() from reaching /usr/local/lib/php/pearcmd.php or other files outside the allowed tree. Deploy via php.ini or .htaccess. Verify with phpinfo(). Note: this can break plugins that access files outside docroot.- IP-based blocking of known attackers — CrowdSec observed 30K+ IPs in 5 days with 100x source growth; attackers rotate through rented VPS and compromised hosts faster than blocklists update.
- WordPress application-level firewalls (e.g., Wordfence free tier) — the vulnerability is in Core's template resolution, which executes before most plugin-based firewalls initialise their request inspection. Premium/paid tiers with early-load virtual patching may help, but free tiers run too late in the request lifecycle.
- Disabling XML-RPC or REST API — this vulnerability is triggered through normal page requests (the
pagenamequery parameter on frontend page loads), not through XML-RPC or the REST API. Disabling those endpoints has zero effect. - File permission hardening on theme directories — the theme directory only needs to *exist* and be traversable; the attacker does not write to it. Making theme files read-only does not prevent the LFI.
The supporting signals.
| In-the-Wild Exploitation | Confirmed and massive. First exploitation recorded at 11:49 UTC September 22, 2026 — same day as patch. 30,813 unique attacking IPs and 322,680 signals in five days (CrowdSec). 350K+ sites reported compromised by Sep 25 (TechTimes). |
|---|---|
| CISA KEV | Listed September 25, 2026. FCEB remediation deadline: September 28, 2026 (3 days). CISA alert. |
| EPSS | 0.39979 — indicates significant predicted exploitation probability, consistent with observed mass scanning. |
| Proof-of-Concept | Multiple public PoCs. ressl/cve-2026-87902-poc (researcher's lab + exploit), crowsec-edtech/CVE-2026-87902 (Python scanner), bhideki/CVE-2026-87902, oliveiralimajr/CVE_2026_87902. Nuclei template merged as projectdiscovery/nuclei-templates#17302 on Sep 23. |
| CVSS Vector | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H — 8.1. Network-reachable, no privileges, no interaction. AC:H reflects the RCE preconditions (theme dir + pearcmd + register_argc_argv). Note: NVD CVSS v4.0 scored 9.2. |
| Affected Versions | WordPress 4.7.0 through 7.1.1 — all branches. WordPress ≤ 4.6 is EOL and receives no backport. |
| Fixed Versions | 7.1.2, 7.0.6, 6.9.9, 6.8.10, 6.7.9, 6.6.9, 6.5.12, 6.4.12, 6.3.12, 6.2.13, 6.1.14, 6.0.16, 5.9.18, 5.8.17, 5.7.19, 5.6.21, 5.5.22, 5.4.23, 5.3.25, 5.2.28, 5.1.26, 5.0.29, 4.9.33, 4.8.32, 4.7.37. |
| Scanning / Exposure | CrowdSec Live Exploit Tracker counted sources growing from 237 → 23,321 in five days, with attackers rotating IPs across rented VPS and compromised hosts. Attackers use uppercase hex encoding, variable traversal depth (1–12 levels), and multipart/form-data wrapping to evade WAFs. |
| Reporter | Robert Ressl (Wollerau, Switzerland) — reported via WordPress HackerOne program on July 20, 2026. Advisory: GHSA-7hp8-65ch-5whp. |
| Patch Turnaround | Reported Jul 20 → patched Sep 22 (64-day coordinated disclosure). Exploitation began the same day the patch shipped — attackers reverse-engineered the one-line diff. |
Sources.
- CISA KEV Alert — CVE-2026-87902
- CrowdSec Exploitation Tracking Report
- Patchstack — WordPress 7.1.2 Security Release
- Equixly — Path Traversal to RCE via PEAR
- Robert Ressl — Researcher PoC and Lab
- Nuclei Template PR #17302
- TechTimes — 350K Sites Compromised
- SecurityWeek — Exploited Immediately After Disclosure
Why this verdict
- Active mass exploitation obliterates AC:H. The vendor's 8.1 scores
AC:Hbecause RCE needs three conditions. But 30K+ IPs are spraying automated exploits and succeeding at scale — the conditions are met far more often than a textbook probability model implies. When commodity Nuclei templates land RCE within 24 hours of disclosure, the practical attack complexity is LOW. - LFI is unconditional and pre-auth. Even without the RCE chain, *every* unpatched WordPress (4.7–7.1.1) is vulnerable to Local File Inclusion with zero prerequisites. Including server-side PHP files can leak database credentials, session secrets, and plugin configuration. The vendor CVSS doesn't adequately weight the standalone LFI impact.
- RCE conditions are default-on in major hosting stacks. Official PHP Docker images ship
register_argc_argv=Onand includepearcmd.php. cPanel on PHP < 8.5 enables it. Several top-20 themes (Twenty Twelve, Twenty Fourteen, Neve, Hestia, Sydney) carry thepage-templates/directory. The intersection is not rare — it's a significant subset of the install base. - WordPress install base is enormous. WordPress powers ~43% of websites globally. The vulnerable version window (4.7–7.1.1) spans nine years. Even if only 10% of instances meet all RCE conditions, that's millions of exploitable servers.
- KEV-listed with 3-day FCEB deadline. CISA added this Sep 25 with the shortest possible remediation window, signalling maximum urgency.
- Role multiplier: WordPress is canonically *internet-facing web infrastructure* — most instances sit directly on the network edge or behind a CDN/reverse proxy. (a) *Low-value role*: dev/staging sites — chain succeeds, blast radius is host-level. (b) *Typical role*: corporate marketing/blog/intranet — chain succeeds, blast radius is web-server compromise + database credential theft + pivot point into internal network. (c) *High-value role*: WordPress as e-commerce platform (WooCommerce handling payment/PII data), membership sites with credential stores, or WordPress Multisite hosting dozens of tenant sites — chain succeeds, blast radius is mass data exfiltration and tenant compromise. While WordPress is not in the canonical IdP/hypervisor/PAM high-value catalog, its universal internet exposure and the unconditional pre-auth LFI make the effective blast radius comparable to a network-edge appliance vulnerability.
Why not higher?
A 10.0 would require unconditional RCE with no preconditions and no mitigating factors. Here, the full RCE chain genuinely requires three conditions to align (theme directory, pearcmd, register_argc_argv). Hosts with hardened PHP configs, managed WordPress hosting (WP Engine, Kinsta), or themes without page-* directories are exposed only to LFI, not RCE. The conditional nature of the worst outcome caps this at 9.0 rather than 9.8–10.0.
Why not lower?
Even setting aside the RCE chain entirely, the unconditional pre-auth LFI on the most popular CMS on earth is itself HIGH severity — it enables inclusion of any readable .php file on the server. Adding the proven, actively-exploited RCE chain that works by default on Docker and cPanel stacks, plus KEV listing and 350K+ compromised sites, makes anything below CRITICAL an indefensible position for a practitioner advisory. The scale of active exploitation alone overrides any theoretical friction discount.
Crowdsourced verification payload.
Run this script on each WordPress host as any user with read access to the WordPress installation directory. Invoke with: bash check_cve_2026_87902.sh /var/www/html (pass the WordPress root path as the first argument). No network access or elevated privileges required — it reads wp-includes/version.php and checks local PHP/theme conditions.
#!/usr/bin/env bash
# check_cve_2026_87902.sh — Detect CVE-2026-87902 exposure
# Usage: bash check_cve_2026_87902.sh /path/to/wordpress
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
set -euo pipefail
WP_ROOT="${1:-}"
if [[ -z "$WP_ROOT" || ! -d "$WP_ROOT" ]]; then
echo "UNKNOWN — Usage: $0 /path/to/wordpress"
exit 2
fi
VERSION_FILE="$WP_ROOT/wp-includes/version.php"
if [[ ! -f "$VERSION_FILE" ]]; then
echo "UNKNOWN — $VERSION_FILE not found; is this a WordPress installation?"
exit 2
fi
# Extract WordPress version
WP_VERSION=$(grep -oP "\\\$wp_version\s*=\s*'\K[^']+" "$VERSION_FILE" 2>/dev/null || true)
if [[ -z "$WP_VERSION" ]]; then
echo "UNKNOWN — could not parse WordPress version from $VERSION_FILE"
exit 2
fi
echo "[*] WordPress version detected: $WP_VERSION"
# Known fixed versions per branch (security backports)
declare -A FIXED_VERSIONS=(
["7.1"]=2 ["7.0"]=6 ["6.9"]=9 ["6.8"]=10 ["6.7"]=9 ["6.6"]=9
["6.5"]=12 ["6.4"]=12 ["6.3"]=12 ["6.2"]=13 ["6.1"]=14 ["6.0"]=16
["5.9"]=18 ["5.8"]=17 ["5.7"]=19 ["5.6"]=21 ["5.5"]=22 ["5.4"]=23
["5.3"]=25 ["5.2"]=28 ["5.1"]=26 ["5.0"]=29 ["4.9"]=33 ["4.8"]=32
["4.7"]=37
)
# Parse major.minor and patch
IFS='.' read -r MAJOR MINOR PATCH <<< "$WP_VERSION"
PATCH=${PATCH:-0}
BRANCH="$MAJOR.$MINOR"
# Check if version is >= 7.2 (future, assumed patched)
if (( MAJOR > 7 )) || (( MAJOR == 7 && MINOR >= 2 )); then
echo "PATCHED — WordPress $WP_VERSION is beyond the affected range."
exit 0
fi
# Check if version is < 4.7 (not affected per advisory — vuln introduced in 4.7)
if (( MAJOR < 4 )) || (( MAJOR == 4 && MINOR < 7 )); then
echo "PATCHED — WordPress $WP_VERSION predates the vulnerable code (introduced in 4.7)."
exit 0
fi
# Look up the fixed patch level for this branch
FIXED_PATCH="${FIXED_VERSIONS[$BRANCH]:-}"
if [[ -z "$FIXED_PATCH" ]]; then
echo "UNKNOWN — branch $BRANCH not in known-fixed table; verify manually."
exit 2
fi
VULN=false
if (( PATCH < FIXED_PATCH )); then
VULN=true
fi
# --- Additional context: check RCE preconditions ---
RCE_RISK=""
# Check for page-* theme directories
THEME_DIR="$WP_ROOT/wp-content/themes"
PAGE_DIR_FOUND=false
if [[ -d "$THEME_DIR" ]]; then
while IFS= read -r -d '' tdir; do
theme_name=$(basename "$tdir")
for subdir in "$tdir"/page-*/; do
if [[ -d "$subdir" ]]; then
PAGE_DIR_FOUND=true
RCE_RISK="${RCE_RISK} [!] Theme '$theme_name' has directory '$(basename "$subdir")/'\n"
fi
done
done < <(find "$THEME_DIR" -maxdepth 1 -mindepth 1 -type d -print0 2>/dev/null)
fi
# Check register_argc_argv
ARGV_ON=false
ARGV_VAL=$(php -r 'echo ini_get("register_argc_argv");' 2>/dev/null || echo "unknown")
if [[ "$ARGV_VAL" == "1" || "$ARGV_VAL" == "On" || "$ARGV_VAL" == "on" ]]; then
ARGV_ON=true
fi
# Check for pearcmd.php
PEAR_FOUND=false
for p in /usr/local/lib/php/pearcmd.php /usr/share/php/pearcmd.php /usr/lib/php/pearcmd.php; do
if [[ -r "$p" ]]; then
PEAR_FOUND=true
PEAR_PATH="$p"
break
fi
done
if $VULN; then
echo "VULNERABLE — WordPress $WP_VERSION is below fixed version $BRANCH.$FIXED_PATCH for CVE-2026-87902."
echo ""
echo "[*] RCE precondition check:"
if $PAGE_DIR_FOUND; then
echo -e "$RCE_RISK"
else
echo " [OK] No page-* theme directories found (RCE theme condition NOT met)."
fi
if $ARGV_ON; then
echo " [!] register_argc_argv is ON — RCE PHP condition met."
else
echo " [OK] register_argc_argv is OFF — RCE PHP condition not met."
fi
if $PEAR_FOUND; then
echo " [!] pearcmd.php found at $PEAR_PATH — RCE gadget present."
else
echo " [OK] pearcmd.php not found in common paths — RCE gadget absent."
fi
if $PAGE_DIR_FOUND && $ARGV_ON && $PEAR_FOUND; then
echo ""
echo " >>> ALL THREE RCE CONDITIONS MET — FULL RCE IS POSSIBLE <<<"
elif $PAGE_DIR_FOUND || $ARGV_ON || $PEAR_FOUND; then
echo ""
echo " >>> PARTIAL RCE conditions met — LFI is still exploitable. Patch urgently. <<<"
else
echo ""
echo " >>> RCE conditions not met, but LFI is still exploitable. Patch required. <<<"
fi
exit 1
else
echo "PATCHED — WordPress $WP_VERSION is at or above the fixed version ($BRANCH.$FIXED_PATCH) for CVE-2026-87902."
exit 0
fi