WordPress shipped the patch in a day. Attackers shipped the exploit in five hours.

A critical path-traversal flaw in WordPress core became a mass campaign within hours of the fix landing. The lesson is not that WordPress is unsafe — it is that patch latency is an architectural property.

Security9 min readPortrait of Oybek Khalikovic, CTO at Karve DigitalBy Oybek Khalikovic
Isometric blueprint illustration: a broad low slab representing the published patch, feeding an escalating three-stage chain of blocks that ends in a single highlighted cube — the deployed webshell.

WordPress core shipped the fix for CVE-2026-87902 — a critical path-traversal flaw in page-template resolution, rated 9.2 on CVSS 4.0 — on 22 September 2026. The first recorded attempt to exploit it arrived at 11:49 UTC the same day, roughly five hours after the release went out.

Nothing in that sequence means WordPress is badly maintained. A one-day turnaround on a critical core bug is a good security outcome, and the fix was backported across nine years of release branches. What the sequence means is that the published patch was itself the exploit specification. Once a fix is public, the window you actually control is not the one between discovery and disclosure — it is the one between the release landing and you having it installed. That window is a property of your architecture and your operations, not of the CMS.

What is CVE-2026-87902?

CVE-2026-87902 is an unauthenticated path traversal in WordPress core's page-template resolution. The get_page_template() function decides which theme file renders a given page. By manipulating the pagename query parameter, an attacker can steer that resolution at a readable PHP file outside the active theme's directories — and PHP then includes and executes whatever it finds there. The upstream advisory, GHSA-7hp8-65ch-5whp, classifies it as CWE-98 — improper control of a filename passed to an include or require statement — and scores it 9.2, critical, under CVSS 4.0.

There is no authentication step, no plugin involved and nothing conspicuous about the request. It is a GET to the site root with a crafted query string, which is why it scaled so easily once the method was public.

  • Affected: every WordPress release from 4.7.0 through 7.1.1.
  • Fixed: 7.1.2, released 22 September 2026.
  • Backported: 7.0.6, 6.9.9, 6.8.10 and 4.7.37 — the advisory states the fix was carried back to every branch as far as 4.7.

Why it isn't automatically remote code execution

Most coverage of this CVE stops at "critical, unauthenticated, remote code execution", and that skips the part that determines whether you are actually at risk. Reaching code execution through this bug requires two conditions to hold at the same time. On a great many installations, neither does.

  1. The active theme contains a top-level directory whose name starts with page-. Template resolution only walks outside the theme when the crafted pagename matches that prefix pattern, so a theme with no such directory gives the traversal nothing to stand on.
  2. A readable PHP file elsewhere on the server provides a file-write primitive. The well-known case is pearcmd.php, which is reachable as a PEAR-to-RCE transition when register_argc_argv is enabled.

Without the first condition, this is still a bug worth removing: an unauthenticated request can influence which file core attempts to load. It just does not reach execution. Without the second, an attacker can make PHP run a file that already exists rather than one they wrote, which is a far narrower position to be in.

Treat both as context for triage order, not as permission to defer the update. Neither precondition is a security control you own with any confidence — you do not decide what a theme update, a child theme or a rebuilt server image adds to your filesystem next month, and nobody treats either change as a security event when they make it. The preconditions tell you how urgent tonight is. They do not tell you the bug is someone else's problem.

How the patch became the exploit blueprint

WordPress publishes its security fixes as code. The 7.1.2 diff showed precisely which encoding of the pagename value the old code mishandled and precisely how the new code neutralises it — which is also an exact description of how to trigger the old behaviour. The payloads seen in the wild matched the specific encoding pattern the patch addressed, which is the signature of attackers working backwards from a diff rather than finding the flaw independently. The technique has a name: patch diffing.

The timing is the argument. Patchstack's data, reported by Help Net Security and SecurityWeek, puts the first exploitation attempt at 11:49 UTC on 22 September, about five hours after 7.1.2 shipped. Tech Times reports that attack traffic against the parameter rose more than tenfold the following day, and that a named Nuclei template was circulating publicly, identifying itself with the user agent nuclei-cve-2026-87902/1.0. Once a working template exists, exploitation stops being a skill and becomes a command-line argument.

Shadowserver data reported by Tech Times puts the number of instances still running an unpatched version at more than 350,000 as of 25 September — three days after the fix was available. WordPress is commonly cited as powering around 43% of all websites, so even a modest unpatched fraction is a large absolute number of reachable targets. The fix existed the entire time. The gap is operational, not technical.

The three-stage attack chain

The observed campaign is methodical rather than clever, and it leaves a trace at every step.

Observed three-stage exploitation chain for CVE-2026-87902

Stage one is cheap reconnaissance: requests to wp-cron.php and to RSS feeds through the vulnerable parameter, establishing whether the traversal resolves at all. Stage two probes for the write primitive by invoking pearcmd.php with +config-show, which reveals whether PEAR is present and reachable. Stage three uses config-create to write a PHP file into a writable temp directory — /tmp or /var/tmp — and then loads it. Observed filenames include wp-pear-rce-flag.php and poc87902.php, alongside randomised names using luci_ and zeta_ prefixes. The important consequence: once stage three completes, updating WordPress does not remove the attacker's access.

Are you affected, and were you already hit?

If you are running any WordPress release from 4.7.0 to 7.1.1, you are affected by the vulnerability — that part needs no investigation. Whether you were targeted is a separate question, and your access logs from 22 September onwards can answer it. Look for three things.

  • pagename values containing %2e%2e or the double-encoded %252e%252e. Percent-encoded traversal sequences have no legitimate reason to appear in a page name.
  • pagename and page_id supplied together on / or /index.php. Normal WordPress routing does not produce that combination.
  • Unexpected PHP files in /tmp and /var/tmp, including the observed names above and anything carrying a randomised prefix.
access.log patterns
# Percent-encoded traversal in the page-template parameter
GET /?pagename=%2e%2e%2f...
GET /?pagename=%252e%252e%252f...

# pagename and page_id supplied together on the site root
GET /?pagename=...&page_id=...
GET /index.php?pagename=...&page_id=...

# Stage 2 — PEAR reachability probe
pearcmd.php ... +config-show

# Stage 3 — webshell written to a temp directory
/tmp/wp-pear-rce-flag.php
/tmp/poc87902.php
/var/tmp/luci_*.php
/var/tmp/zeta_*.php

One caveat worth stating plainly: if your access logs have already rotated past the relevant dates, a clean search is not evidence that nothing happened. In that case the stronger signals are the temp-directory inspection and a file-integrity comparison against a clean checkout of the same version.

Fix it now

In priority order.

  1. Update to WordPress 7.1.2, or to the backport for your branch: 7.0.6, 6.9.9, 6.8.10 or 4.7.37. This closes the vulnerability, and it is the only step that does.
  2. Disable register_argc_argv in your PHP configuration if nothing you run depends on it. That removes the most commonly abused route to the second precondition, and very little legitimate web code needs it.
  3. Apply a WAF virtual patch for the request pattern while the update is being scheduled or tested. This buys time. It does not replace the update.
  4. Audit your access logs for the indicators above and inspect /tmp and /var/tmp for PHP files that should not be there. If you find either, handle it as a compromise rather than a cleanup: rotate credentials and salts and rebuild from a known-good state.

The real lesson: patch latency is architectural

Start from what actually happened. WordPress core took a report of a critical flaw and shipped a fix in about a day, then backported it across branches going back to 4.7. That is a good security response, and any article using this CVE to argue that WordPress is unsafe is arguing the wrong thing. The fix was not slow. The fix was public — and public fixes are exploit specifications, with a five-hour lead time in this case.

Which reframes the question worth asking about your own stack. Not "is this platform secure" — no platform is secure in the abstract, and nobody can promise you a dependency tree without bugs. The question is narrower and answerable: when a fix for something in my stack becomes public at 06:00, what has to happen before it is running in production, and how long does that take? If the honest answer is that someone has to notice, file a ticket, find a staging environment, hope the theme still renders and ship it next sprint, then your exposure is measured in weeks no matter how fast upstream moves.

Time-to-patch is not a discipline problem you can fix with a calendar reminder. It is set by architecture: how much of the runtime is your responsibility rather than your host's, how many third-party plugins sit in the request path, whether the content layer and the rendering layer can be updated independently of each other, and whether you have enough automated coverage to apply an update without a manual regression pass. Those are design decisions, taken long before any CVE, and they are what determine the width of your window.

That is the honest case for migrating, and the only one worth making. Moving off a self-hosted PHP monolith does not eliminate vulnerabilities — it changes who is responsible for patching what, and it shrinks the part of the surface you have to patch yourself. We do that work in three directions: WordPress to Sanity and Next.js, WordPress to Laravel Filament and WordPress to Umbraco. Our comparison of all three destinations sets out where each one fits, what it costs you, and where it is the wrong answer.

And if your WordPress site is already on 7.1.2 and your update path is fast and boring, you do not need any of that. Go and check your logs instead.

Questions
Does updating to 7.1.2 fully fix it?

Yes for the vulnerability, no for a site that was already compromised. Updating to 7.1.2 — or to 7.0.6, 6.9.9, 6.8.10 or 4.7.37 on an older branch — closes the path-traversal flaw so it can no longer be triggered. It does nothing about a webshell that was written to disk before you patched, because that file no longer depends on the bug. If you find unexpected PHP files in /tmp or /var/tmp, or traversal patterns in your access logs from 22 September onwards, treat the update as the first step of an incident response rather than the end of one: rotate credentials and salts, and rebuild from a known-good state.

Am I safe if my theme has no page- directory?

You are not exposed to code execution through this path, because one of the two required preconditions is not met: template resolution needs a top-level theme directory whose name begins with page- before the traversal has anywhere to go. You should still patch. The underlying flaw is that an unauthenticated request can influence which file core tries to load, and your theme layout is not a security control you can rely on over time — a theme update, a child theme, or a switch to a different theme can introduce a matching directory, and nobody will treat that as a security change when it happens.

How do I tell whether I was already compromised?

Search your access logs from 22 September 2026 onwards for pagename values containing %2e%2e or %252e%252e, and for requests passing pagename and page_id together on / or /index.php — normal WordPress routing does not produce that combination. Then inspect /tmp and /var/tmp for PHP files that should not be there; observed names include wp-pear-rce-flag.php and poc87902.php, along with randomised luci_ and zeta_ prefixes. One caution: if your logs have already rotated past the relevant dates, finding nothing is not evidence that nothing happened. Fall back to a file-integrity comparison against a clean checkout of the same version, plus the temp-directory inspection.

Should I migrate off WordPress because of this?

No — not over one CVE. WordPress core shipped a fix in about a day and backported it to branches going back to 4.7, which is a good security response, and every platform will eventually publish a critical fix of its own. Migration is the right call when the problem is structural rather than incidental: when patch latency is consistently measured in weeks because updates are risky to apply, when you are carrying a plugin supply chain nobody has audited and nobody realistically can, or when the content model has drifted so far from what the business needs that it cannot be maintained. If none of those describe you, patch and get on with your work. If several do, this CVE is not the reason to migrate — it is just the event that made the cost of waiting visible.

Which older WordPress versions got backported fixes?

The upstream advisory (GHSA-7hp8-65ch-5whp) lists the patched versions as 7.1.2, 7.0.6, 6.9.9, 6.8.10 and 4.7.37, and states that the fix was backported to every branch as far back as 4.7 as a courtesy to users on older releases. Apply the one matching your branch. A backport closes this particular flaw on an old branch; it does not make an old branch a good long-term position to be in.

Talk to the people
who wrote this.

Let's Talk
Start a project