MP Marc Pope Let's Talk
wp2shell: A Pre-Auth RCE in WordPress Core With a Day-One Public Exploit

wp2shell: A Pre-Auth RCE in WordPress Core With a Day-One Public Exploit

Two chained CVEs in WordPress's REST API batch endpoint let any anonymous request execute code on your server. Patches shipped July 17 — and the PoC hit GitHub the next morning.

Yesterday morning, the WordPress security team did something it almost never does: it force-pushed a security update through the auto-update system without waiting for site owners to approve it. That alone should tell you how serious wp2shell is. By the time most people read about it, WordPress was already attempting to patch their sites for them — and by this morning, a working proof-of-concept exploit was published to GitHub.

This one matters. I've spent decades running hosting infrastructure where WordPress is installed by the millions. A pre-authentication remote code execution flaw in core, requiring no plugins, no special configuration, and no account — just a single crafted HTTP request — is about as bad as it gets in this ecosystem. Let me explain exactly what's happening, why the patch works, and what you need to check if you're running WordPress anywhere.

The Two CVEs Behind wp2shell

wp2shell is actually two vulnerabilities that chain together for maximum effect. On their own, each is dangerous; together, they allow full unauthenticated code execution on a default WordPress installation.

CVE-2026-60137 is a SQL injection in WP_Query. Specifically, the author__not_in parameter is supposed to accept an array of integer user IDs. When a string is passed instead of an array, the validation logic silently coerces it rather than rejecting it, and the raw value flows into a database query without proper sanitization. This bug exists in WordPress 6.8 and later — which means the 6.8 branch gets its own fix in 6.8.6, even though 6.8 sites aren't exposed to the full RCE chain.

CVE-2026-63030 is the escalation that turns that SQL injection into remote code execution. It lives in the REST API batch endpoint, introduced in WordPress 6.6 but not combined with this injection surface until 6.9 shipped on December 2, 2025. The batch endpoint lives at /wp-json/batch/v1 (also reachable at /?rest_route=/batch/v1) and lets a caller bundle multiple REST sub-requests into a single HTTP round-trip — a useful performance optimization that turns out to have a subtle and fatal bookkeeping flaw.

The Route Confusion at the Heart of the Bug

The batch handler internally maintains three parallel arrays as it processes sub-requests: $requests (the raw input), $validation (the result of permission checks), and $matches (which route each request was matched to). The assumption is that these arrays stay in sync — entry zero of $validation describes the permissions for the route stored in entry zero of $matches.

When a sub-request fails early parsing — for instance, because its URL is malformed, like the literal string http:: — the code records a failure into $validation but skips adding an entry to $matches. From that point on, the arrays are off by one. Every subsequent sub-request is dispatched against the handler for the next route in the list, using the permission callback for the previous one.

An attacker who crafts the sub-request order carefully can ensure that a privileged, write-capable endpoint executes under the permission callback for an endpoint that allows anonymous access. The result, as the Hacker News writeup puts it plainly: "an anonymous HTTP request can run code on a WordPress site" with "no preconditions."

The CVSS score for CVE-2026-63030 is 9.8 — Critical. That's not inflated. No authentication. No plugins. No configuration flags. A fresh WordPress 6.9 install, straight out of the box, is exploitable.

What the Patch Actually Does

The fix, per the researchers at Hadrian who discovered it, is two lines of code. The batch handler now records a placeholder into $matches whenever a sub-request fails early parsing, keeping the three arrays in sync. A second change adds a re-entrancy guard via an is_dispatching() method to prevent other edge cases in batch processing from producing the same class of misalignment.

Two lines. That's the entire surface area of a flaw that has been sitting in production on tens of millions of sites since December 2, 2025. WordPress 6.9 shipped 228 days ago. If your site has been on any 6.9.x or 7.0.x release, this hole has been open for months.

Who Is Affected

The full RCE chain — both CVEs working in concert — requires WordPress 6.9.0 through 6.9.4, or 7.0.0 through 7.0.1. Sites on WordPress 6.8.x are exposed to the underlying SQL injection (CVE-2026-60137) but not to the route confusion that completes the code execution chain; they should still patch to 6.8.6.

There is one partial mitigation: sites running a persistent object cache (Memcached, Redis, or similar) are reportedly not exploitable for the full RCE even on vulnerable versions, because the cache layer changes the code path through which the injection fires. I'd treat that as a lucky coincidence, not a defense strategy. Patch anyway.

The attack works on both /wp-json/batch/v1 and /?rest_route=/batch/v1, so blocking one URL doesn't help if the other is accessible. And the batch API isn't something most sites have consciously enabled — it's on by default as part of core's REST infrastructure since 6.6.

WordPress's Unusual Response

The WordPress.org team enabled forced background updates for this release. Normally, auto-updates apply to minor security releases but require that site owners haven't disabled the mechanism. For wp2shell, the team pushed the update through their distribution infrastructure with unusual urgency — a signal that they believed the threat of active exploitation was immediate.

They were right to be concerned. As of this morning, a working, non-destructive proof-of-concept is publicly available on GitHub. The PoC uses a safe probe — a batch request with a malformed first entry, followed by a DELETE against /wp/v2/categories/0 and a POST to the block-renderer route — to confirm whether a target is vulnerable without causing damage. That's the responsible-disclosure version. Weaponized variants won't be far behind.

If your site has auto-updates disabled — and a lot of sites do, because agencies and developers need to test updates before deploying them — you did not receive this patch automatically. You need to check and update manually.

What to Do Right Now

The action list is short:

  1. Check your WordPress version. If you're on 6.9.0–6.9.4 or 7.0.0–7.0.1, you are vulnerable. Update to 6.9.5 or 7.0.2 immediately.
  2. Check your 6.8.x sites too. They need 6.8.6 for the SQL injection fix.
  3. Audit your update policies. If you've disabled auto-updates, confirm that your update workflow can respond to critical vulnerabilities within hours, not weeks. This incident is a good stress-test of that process.
  4. Review your WAF rules. Cloudflare published WAF rules for both CVEs shortly after the patches dropped. If you're behind Cloudflare, verify those rules are active. Other WAF vendors should have followed within hours.
  5. Check your logs. The attack pattern is a POST to /wp-json/batch/v1 or /?rest_route=/batch/v1 with a malformed first sub-request URL. If you see anomalous batch API traffic from the past 24 hours, investigate.

The Broader Lesson About REST API Attack Surface

I want to flag something about this vulnerability that goes beyond WordPress specifically. The batch endpoint exists to improve performance — bundle multiple requests, save round-trips. It's a reasonable feature. The bug isn't in the concept; it's in the bookkeeping, a subtle array synchronization assumption that held until it didn't.

This is exactly the class of bug that security review tends to miss because it requires reasoning about failure paths across an entire multi-step dispatch loop, not just about any individual function. Static analysis tools won't catch it. A code review focused on "does this function sanitize its inputs" won't catch it. You need someone tracing the invariants across the full lifecycle of a batch request — and that's expensive, which is why it rarely happens for infrastructure-level framework code.

The practical implication for anyone who builds or runs platforms: your attack surface is not just the code you wrote. It's every REST endpoint, every batch processor, every permission callback dispatcher in every dependency you run. WordPress is the most-installed web software on the planet, maintained by a large and experienced security team, and it still shipped a 9.8 CVSS flaw that sat undetected for seven months. That's not an indictment of WordPress — it's a description of how hard this problem is.

What it means operationally: WAF coverage for your application layer needs to be active, your update turnaround on critical patches needs to be measured in hours, and your log monitoring needs to cover REST API anomalies, not just traditional attack patterns like SQLi in form fields. The interesting attacks are increasingly happening in the infrastructure plumbing, not the user-facing forms.

Patch first. Then come back and read the PoC — it's a clean piece of work and worth understanding if you operate WordPress at any scale.

Back to Blog