CVE-2026-63030 & CVE-2026-60137: wp2shell: Pre-Authentication RCE in WordPress Core via REST Batch-Route Confusion and SQL Injection
A pre-authentication RCE in WordPress core, not a plugin, not a theme, the engine itself, is about as high-impact as web bugs get. WordPress runs a large share of the public web, and on July 17, 2026 the security team shipped an emergency release, 6.9.5 and 7.0.2, then took the rare step of forcing automatic updates onto affected sites. The trigger was a bug chain nicknamed wp2shell, reported by Adam Kues of Assetnote (Searchlight Cyber) through WordPress’s HackerOne program.
What makes wp2shell notable is not a single clever primitive but the composition: a moderate-rated SQL injection that is uninteresting on its own becomes a critical pre-auth RCE once it is reached through a second flaw in the REST API’s batch endpoint. A default install (no plugins, no special configuration, no authenticated session) is exploitable by a single anonymous HTTP request. This post walks through what is publicly known about the two CVEs, how they chain, how to determine whether you are exposed, and what to do about it.
Vulnerability Overview
wp2shell is tracked as two distinct CVEs that are only dangerous together:
- CVE-2026-60137 (GHSA-fpp7-x2x2-2mjf): a SQL injection in the
author__not_inparameter of the internalWP_Queryclass. Rated Moderate in isolation, because on a stock install there is no unauthenticated code path that feeds attacker input into that parameter. - CVE-2026-63030 (GHSA-ff9f-jf42-662q): a REST API batch-route confusion weakness at
/wp-json/batch/v1. Rated Critical, because it is what lets an anonymous request reach the injection above and turn it into remote code execution.
The affected and fixed versions differ between the two, which matters for triage:
- 6.8.0 – 6.8.5: SQL injection only (CVE-2026-60137). No RCE chain on this branch. Fixed in 6.8.6.
- 6.9.0 – 6.9.4: full RCE chain. Fixed in 6.9.5.
- 7.0.0 – 7.0.1: full RCE chain. Fixed in 7.0.2.
- 7.1 beta: fixed in 7.1 beta2.
- Anything below 6.8.0 is not affected by the RCE.
The distinction is worth restating because it is easy to misread: the batch-route confusion (the RCE-enabling half) only exists on the 6.9 and 7.0 branches. A 6.8.x site has the injection but not the delivery mechanism, so it should be treated as an important-but-not-emergency SQLi patch. A 6.9.x or 7.0.x site is a pre-auth RCE and should be treated as an incident.
Background: The REST API Batch Endpoint
Since WordPress 5.6, core has shipped a batch controller registered at /wp-json/batch/v1. Its job is to accept several REST sub-requests in a single HTTP call and dispatch them internally, returning an array of responses that line up positionally with the array of requests that came in. It exists to cut round-trips for clients like the block editor that need to fire many small API calls at once.
Two properties of that design are relevant here. First, the batch controller re-dispatches each sub-request through the internal REST machinery rather than over the network, so the sub-requests inherit the dispatch context of the batch call. Second, the controller maintains parallel arrays (the incoming requests on one side, the tracking and response state on the other) and relies on those arrays staying aligned by index as it iterates. Any endpoint that maintains positional correspondence between two collections is a place to look for confusion bugs, and that is precisely the class CVE-2026-63030 falls into.
CVE-2026-60137: SQL Injection in WP_Query’s author__not_in
WP_Query is the core class that translates high-level query arguments into SQL against the posts table. Several of its arguments accept lists of IDs (post__in, post__not_in, author__in, author__not_in) and the long-standing contract is that these are passed as arrays of integers. When the value is an array, core casts each element with absint() before building the IN (...) / NOT IN (...) clause, which is why these parameters have historically been safe.
The root cause of CVE-2026-60137 is a type-handling gap: when author__not_in receives a string instead of an array, the per-element integer casting that protects the array path is bypassed, and the value flows into the generated SQL without the sanitization the code assumes has already happened. In other words, the defense was correct for the expected shape of the input and silently absent for an unexpected one: a classic “validate the array, forget the scalar” defect. The fix normalizes the parameter so the integer-casting invariant holds regardless of whether a caller passes a string or an array.
On its own this is only Moderate severity, and the reason is important: on a stock WordPress site there is no unauthenticated, attacker-controlled path that hands a raw string to author__not_in. The parameter is reachable through internal query construction and through some authenticated flows, but not, by itself, from an anonymous HTTP request. That gap is exactly what the second bug closes.
CVE-2026-63030: Batch-Route Confusion
The batch controller’s contract is that request N in the input maps to permission check N, dispatch N, and response N. CVE-2026-63030 is a route confusion bug: an attacker can craft a batch payload such that the controller’s internal bookkeeping arrays fall out of alignment, and a sub-request is dispatched under a context (routing, and by extension the permission and parameter handling associated with a different slot) than the one it should have been bound to.
The practical consequence is a confused-deputy condition: the batch endpoint, which is itself reachable without authentication for the sub-routes it exposes, becomes a vehicle for driving an internal query with attacker-chosen parameters that would not normally be reachable anonymously. That is what promotes the Moderate SQLi to a Critical, no-preconditions RCE: the batch route supplies the missing unauthenticated path to author__not_in.
Assetnote held the granular exploitation details (the exact batch payload shape and the specific misalignment primitive) at disclosure, publishing a non-exploitative checker at wp2shell.com instead, so sites could patch first. We keep the raw payload out of the prose above for the same reason: the mechanism is enough to reason about exposure and defense. With the fix now public, our own runnable proof-of-concept and a self-contained lab are linked below for authorized validation.
Composing the Two: From Anonymous Request to Shell
The chain, at the level of abstraction that matters for defenders, is:
- An anonymous request hits
/wp-json/batch/v1with a crafted batch body. - The batch-route confusion (CVE-2026-63030) desynchronizes the controller’s per-slot tracking, so a sub-request is dispatched with parameters and a context it should never have had anonymously.
- That mis-dispatched sub-request reaches an internal
WP_Querywith a stringauthor__not_invalue under attacker control. - The type-handling gap (CVE-2026-60137) lets that string reach the SQL layer, giving injection into the query against the posts table.
- From SQL injection, the attacker escalates to remote code execution: the standard routes being object-injection through unserialized data, writing attacker-controlled option/meta values that are later executed, or otherwise leveraging DB write/read primitives that WordPress’s own architecture exposes to a sufficiently capable injection.
The reason WordPress rated the composite Critical and forced auto-updates is step 1: there are no preconditions. No account, no nonce, no configuration toggle. If the site is on an affected 6.9.x or 7.0.x version and the REST API is reachable (which, on a default install, it is), the site is exploitable.
Watch: The Chain, Start to Shell
Our research team walked the chain end to end: the anonymous request to the batch endpoint, the route desync, and the injection landing against the posts table. The demo shows the reachability seam the two CVEs open up, enough to see how a stock install falls.
Proof of Concept
Now that the patched releases are out, we have published a working proof-of-concept for authorized testing: github.com/securelayer7/WordPresShell. It chains CVE-2026-63030 and CVE-2026-60137 into unauthenticated admin creation and a webshell, and ships with a Docker Compose lab (a vulnerable 7.0.1 alongside a patched 7.0.2) so you can reproduce the bug and confirm the fix without touching anything you do not own.
The tool has three modes: verify (a non-destructive reachability check), dump (read from the database), and exec (single or interactive command execution). Authorized testing only. Run it against systems you own or have written permission to assess. The fastest safe use is to point verify at the bundled lab, or at your own staging host, to confirm your patch status.
Am I Affected?
Check your core version first. From the dashboard it is at the bottom-right of any admin screen; from the CLI:
wp core version
Map the result against the table above. If you are on 6.9.0–6.9.4 or 7.0.0–7.0.1, treat it as a live pre-auth RCE exposure. If you are on 6.8.0–6.8.5, you have the SQLi but not the RCE chain. Still patch, but the urgency profile is different. If you are already on 6.8.6, 6.9.5, 7.0.2, or later, you are patched.
You can also confirm the batch endpoint is exposed at all, since many hardened deployments already block or disable it:
curl -s -o /dev/null -w "%{http_code}\n" https://YOUR-SITE/wp-json/batch/v1
Assetnote’s checker at wp2shell.com performs a non-destructive test against your own instance if you prefer an external confirmation. As always, only test systems you own or are authorized to assess.
Detection
Because the entry point is a specific, rarely-hit core route, this chain leaves reasonably distinctive traces. In access logs, look for:
- Unauthenticated
POSTrequests to/wp-json/batch/v1(or the query-string form/?rest_route=/batch/v1), particularly from IPs with no prior authenticated session. - Batch bodies containing unusually large or malformed request arrays, or sub-requests referencing post/author query parameters.
- SQL errors or anomalous query latency in the database log correlated with those requests; injection attempts frequently generate parse errors before a working payload lands.
- Post-exploitation indicators: new admin users, unexpected changes to the
active_pluginsoption, new files underwp-content/uploadswith a.phpextension, or modifications towp-config.phpand theme files.
If you find evidence of exploitation rather than just probing, treat it as a full compromise: an RCE at the WordPress process level generally means the database, all stored secrets, and any credentials reachable from the host should be considered exposed. Patching after the fact does not evict an attacker who already has a shell or a persistence mechanism.
Mitigation
Update core. That is the fix. Move to 6.8.6, 6.9.5, or 7.0.2 (or newer) for your branch. WordPress enabled forced auto-updates for affected versions, so many sites are already patched, but confirm rather than assume, especially where auto-updates were disabled via WP_AUTO_UPDATE_CORE, a constant in wp-config.php, or a management plugin.
If you genuinely cannot patch immediately, the following reduce exposure as temporary cover only. None is a substitute for the update:
- Block the batch route at the edge. A WAF or reverse-proxy rule that denies
POSTto/wp-json/batch/v1(and the?rest_route=/batch/v1equivalent) removes the RCE delivery path. This is the highest-value single mitigation and is low-risk for most sites, which do not depend on the batch endpoint for front-end functionality. - Disable the batch endpoint with a must-use plugin. A small mu-plugin can unregister the
batch/v1route or require authentication on it, closing the anonymous path without touching the rest of the REST API. - Restrict unauthenticated REST access more broadly if your architecture allows it, for example requiring authentication on
/wp-json/where no anonymous consumer needs it.
Edge blocking is preferable to application-layer filtering here because it keeps the crafted request away from the vulnerable code entirely. Whichever stopgap you choose, schedule the core update as the actual remediation and remove the workaround afterward so it does not silently break the block editor or other legitimate batch consumers later.
The Broader Lesson
wp2shell is a clean illustration of a pattern we see repeatedly in mature codebases: two individually-unremarkable bugs that are each correctly triaged as low or moderate severity, right up until someone finds the edge that connects them. The SQL injection was “safe” because nothing unauthenticated could reach it. The batch route was a “confusion” bug whose impact depended entirely on what it could be pointed at. Neither team’s severity rating was wrong in isolation; the danger lived in the seam between them.
This is also why type-handling gaps deserve more respect than they usually get. “Validate the array, forget the scalar” sounds like a cosmetic inconsistency until an unexpected input shape reaches it through a path the original author never modeled. Defensive coding that normalizes input at the boundary, coercing to the expected type before any trust decision, is what turns these seams into dead ends instead of exploitation primitives.
Impact
A pre-authentication RCE in WordPress core is, by reach alone, one of the most consequential web vulnerabilities of 2026. It requires no credentials, no user interaction, and no non-default configuration, and it targets software that fronts a very large fraction of the internet. WordPress’s decision to force automatic updates was proportionate to that reach.
For defenders the action is unambiguous: confirm your core version, patch to the fixed release for your branch, and if you cannot patch on the spot, block /wp-json/batch/v1 at the edge until you can. Then check your logs for the request signatures above. A forced patch protects you going forward, but it says nothing about whether someone reached you in the window before it landed.
Credit
The RCE chain (CVE-2026-63030, the REST batch-route confusion) was found and reported by Adam Kues of Assetnote / Searchlight Cyber, through WordPress’s HackerOne program. The underlying SQL injection (CVE-2026-60137, WP_Query‘s author__not_in) is credited to TF1T, dtro, and haongo. The fixes landed in the WordPress core 6.8.6, 6.9.5, and 7.0.2 security releases; the maintainers coordinated the disclosure and forced auto-updates. This write-up is our analysis of their work. Assetnote held their granular exploit at disclosure so sites could patch; with the fixed releases now shipped, our own proof-of-concept (linked above) is provided for authorized testing.
SecureLayer7’s research team finds chains like wp2shell before they ship: the seam between two “moderate” bugs that no single scanner flags. If WordPress is load-bearing in your estate, book a scoping call. We’ll test for the unauthenticated-reachability paths a version bump never shows you.