WordPress Backported a Fix to 4.7. Attackers Needed 11 Hours.

WordPress published 7.1.2 on 22 September 2026. Patchstack logged the first exploitation attempts against the flaw it fixed at 11:49 UTC the same day. By 23 September attackers had stopped fingerprinting and started writing PHP files to disk. On 25 September CISA added CVE-2026-87902 to its Known Exploited Vulnerabilities catalog with a remediation deadline of 28 September — and, unusually, with forensicTriage: Yes, meaning federal agencies must triage for compromise rather than simply patch and move on.

The WordPress advisory, GHSA-7hp8-65ch-5whp, is rated Critical, CVSS 9.2, and the fix was backported across 25 release branches, all the way back to 4.7.37. That backport list is the clearest statement of severity in the whole disclosure: the affected range is 4.7.0 through 7.1.1, which is very close to "every WordPress site that has ever been updated in the last nine years."

The bug: a sanitiser that preserves what it should destroy

The advisory describes it plainly: an unauthenticated attacker can make get_page_template() page-template resolution include a chosen readable local .php file outside the active theme directories. NVD classifies it as CWE-98 and CISA's KEV entry calls it remote file inclusion; in practice it is a local file inclusion that reaches code execution when the right files exist on disk.

The mechanism Patchstack documents from live traffic is worth understanding because it explains why generic traversal rules miss it. WordPress runs the slug through its own sanitiser before the template candidate is built, and that sanitiser deliberately preserves escaped octets while rewriting literal dots and truncating at literal slashes. A plain ../../ payload does not survive. A percent-encoded one does — and then get_page_template() decodes it. Hence every observed payload arrives encoded, and %2e%2e is the high-signal string to search logs for.

The second structural detail: requests pair pagename with a page_id that resolves to a real page. Without it WordPress serves a 404 and the template code path is never reached. Patchstack's read is that whoever built the first payloads understood the code path well enough to know traversal alone is insufficient — and that the co-occurrence of those two parameters, rare in ordinary traffic, is itself a detection signal.

The preconditions are narrower than the version range, and still common

The GitHub advisory lists them explicitly. The active child or parent theme must contain a top-level directory whose name starts with page-; the advisory names Twenty Twelve and Twenty Fourteen plus popular third-party themes Neve, Hestia, and Sydney. And a chosen local .php file must exist and be readable by the web server account.

That second condition is where the well-known pearcmd.php PEAR-to-RCE transition enters, and the advisory is blunt about who ships it: the transition works when register_argc_argv is On, and "the official php image for Docker is affected, and the default cPanel configuration is affected when PHP prior to 8.5 is in use." The two most common ways to run PHP — the official container and mainstream shared hosting — are the vulnerable defaults.

Three stages, in under 24 hours

Patchstack's firewall telemetry maps a textbook escalation:

  • Stage one — is the inclusion live. The inclusion is pointed at an ordinary core file so the response reveals vulnerability. wp-links-opml.php was the most common target, with wp-includes/feed-rss2.php, wp-cron.php, wp-includes/functions.php, wp-login.php and install.php alongside.
  • Stage two — is pearcmd reachable. The inclusion is pointed at pearcmd.php with +config-show appended. With register_argc_argv enabled, the query string is exposed to the included script as $argv, so the response confirms both that PEAR is present and that the argv trick works. Three install paths are being tried — /usr/local/lib/php/, /usr/share/php/, /usr/share/pear/ — covering most distributions and container images.
  • Stage three — writing files. config-show becomes config-create, which pearcmd will use to write a file wherever it is told with attacker-controlled content. Arbitrary file write with controlled PHP content is code execution.

Patchstack notes the observed drops land in /tmp and /var/tmp — not usually web-reachable, so these are proofs of execution rather than persistent backdoors. That distinction is small comfort: the same primitive writes somewhere more useful whenever the operator decides to. Observed filenames include wp-pear-rce-flag.php, poc87902.php, and randomised luci_* / zeta_* variants; some payloads write only a marker string (host-list building), others write a tag that executes a shell command on access.

Commoditised within a day

Two user agents in the traffic settle the question of who is exploiting this: cve-2026-87902-poc/1.0 and nuclei-cve-2026-87902/1.0. A named Nuclei template means the exploit is no longer confined to operators reading the patch diff — it is in general circulation and can be pointed at any host list. Most remaining traffic spoofs browser user agents, so UA is not a filter. Source addresses went from a small cluster on the first evening to a few hundred, which retires IP blocklisting as a strategy.

Patchstack also warns against anchoring detection too tightly: observed variation includes traversal depths from one to twelve levels, uppercase and lowercase hex, single and double encoding, POST as well as GET (WordPress reads pagename from the POST body in preference to the query string, and POST has since overtaken GET), and requests sent directly to /index.php as well as the site root.

The eleven-hour number is the story

The gap between a public patch and working exploitation keeps collapsing, and this case is close to the floor. The payloads Patchstack saw on day one matched the exact encoding the patch addresses — meaning they were derived from the diff, not from independent discovery. Publishing a fix for a pre-auth flaw in the most deployed CMS on the internet is, functionally, publishing the vulnerability.

This is the same dynamic as the Orkes Conductor pre-auth RCE where the patch shipped in June and the exploit wave arrived in September, only compressed from three months to half a day — a function of install-base size and template availability, not of anything WordPress did wrong in its disclosure. For anyone running WordPress in front of AI tooling — and plenty do, from MCP plugins that expose site administration to agents to content pipelines with model API keys in wp-config.php — the exposure is not the blog. It is every credential the PHP process can read.

What to do

  • Update now, on whatever branch you are on. 7.1.2, 7.0.6, 6.9.9, 6.8.10, 6.7.9, 6.6.9, 6.5.12 and so on down to 4.7.37. The backport exists precisely so "we can't jump major versions" is not a reason to stay exposed.
  • Assume compromise if you were exposed past 22 September. CISA flagged this one for forensic triage. Search /tmp and /var/tmp for recently created .php files, and the web root for anything you did not put there.
  • Grep logs for the parameter pair, not just the traversal. pagename co-occurring with page_id, plus any encoded %2e%2e / %252e sequence, across GET and POST bodies, to both / and /index.php.
  • Turn off register_argc_argv. It is the hinge that converts file inclusion into code execution via pearcmd, it is on in the official PHP Docker image, and almost no web application needs it. Removing PEAR from production images closes the same door.
  • Rotate secrets reachable by the PHP process. Database credentials, API keys in wp-config.php, and any model or service token a plugin stores — on any host that showed stage-two or stage-three traffic.

Sources: