wp2shell: Inside the WordPress Core Pre-Auth RCE (CVE-2026-63030) and How to Respond
Summary
On 17 July 2026, the WordPress project shipped an emergency security release for wp2shell, a pre-authentication remote code execution vulnerability in WordPress Core itself, not in a plugin, theme, or misconfiguration. A stock install with no plugins and default settings is exploitable by a single anonymous HTTP request.
WordPress runs roughly 43% of all websites. The flaw was discovered by Adam Kues of Assetnote (part of Searchlight Cyber) and reported through WordPress’s HackerOne programme, with a related SQL injection issue reported separately by other researchers. WordPress.org enabled forced automatic updates for affected versions, a step it reserves for the most severe bugs.
Technical details were withheld at disclosure to give administrators time to patch. This article reconstructs the root cause from the public patch and advisories, explains why the bug is easy to reach, and sets out how to detect and remediate it. It contains no working exploit.
Technical overview
| Field | Detail |
|---|---|
| Name | wp2shell |
| CVEs | CVE-2026-63030 (REST batch route confusion leading to RCE), CVE-2026-60137 (SQL injection in author__not_in) |
| Class | CWE-284 Improper Access Control leading to Remote Code Execution |
| Affected | WordPress Core 6.9.0 to 6.9.4 and 7.0.0 to 7.0.1 |
| Not affected | 6.8.5 and earlier (the RCE chain; the underlying SQLi issue is separate) |
| Fixed in | 6.9.5 and 7.0.2 (6.8.6 ships the same hardening) |
| Preconditions | None. Unauthenticated, no plugins, default configuration. |
| CVSS | 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) |
How it works
wp2shell abuses the REST API batch endpoint, a core feature that lets a client bundle several REST calls into one request. It is on by default, reachable without authentication, and does not depend on pretty permalinks, because it answers on both of these paths:
/wp-json/batch/v1
/?rest_route=/batch/v1
The second form is available even on installs that disable URL rewriting. If a site runs an affected version and exposes the REST API, which nearly all do, it is in scope.
The batch handler resolves each sub-request to a route and handler, validates them, then dispatches them in a second pass. To do this, WP_REST_Server keeps three parallel lists (the requests, their validation results, and their matched handlers) and depends on all three staying index-aligned.
The bug is that a malformed sub-request breaks that alignment. When a sub-request fails early, for example a path that cannot be parsed as a URL, the error is recorded in the validation list but the loop calls continue without appending anything to the matched-handlers list:
foreach ( $requests as $single_request ) {
if ( is_wp_error( $single_request ) ) {
$has_error = true;
$validation[] = $single_request; // recorded here...
continue; // ...but NOT in $matches
}
$match = $this->match_request_to_handler( $single_request );
$matches[] = $match;
// ...permission and validation checks...
}
From that point on, $matches is one element shorter than $requests. The dispatch loop then indexes straight into it by position:
foreach ( $requests as $i => $single_request ) {
$match = $matches[ $i ]; // off by one for every request after the error
list( $route, $handler ) = $match;
$result = $this->respond_to_request( $single_request, $route, $handler, $error );
}
Every sub-request after the deliberately broken one is now dispatched against the route, handler, and permission callback that belong to a different sub-request. Because the attacker controls the order of sub-requests in the batch, they control which request object gets evaluated against which permission check.
A benign, low-privilege request can be lined up to satisfy the permission gate that a privileged handler was supposed to enforce, and the privileged handler then runs anyway. The server checked the wrong request’s permission. Chained into a sink reachable through the batch route, that handler confusion is what turns into unauthenticated code execution. The separate SQL injection in the author__not_in query parameter (CVE-2026-60137) sits in the same neighbourhood of reachable functionality and widens what an attacker can do once the permission model is subverted.
The fix
Core keeps the arrays aligned by recording the error in $matches as well, and adds a re-entrancy guard so a sub-request can no longer start a fresh top-level REST cycle mid-dispatch:
public function serve_request( $path = null ) {
+ if ( $this->is_dispatching() ) {
+ return false;
+ }
foreach ( $requests as $single_request ) {
if ( is_wp_error( $single_request ) ) {
$has_error = true;
+ $matches[] = $single_request; // keep arrays aligned
$validation[] = $single_request;
continue;
}
Detecting it
The exploit is hard to catch in traffic. It is a single POST to a legitimate core endpoint with a well-formed JSON body, almost indistinguishable from the batch requests the block editor sends during normal editing. It carries no shell metacharacters, no path traversal, and nothing large enough to trip a length rule. Removing the WordPress generator version string, a common hardening step, also strips the one passive signal that would flag an affected version, so any detection that reads the version off the homepage will miss hardened installs.
Because the flaw is a property of how core dispatches the batch rather than of any version string, you can confirm it directly and without changing anything on the target. A crafted batch produces a response that only a vulnerable core returns. The public checker at wp2shell.com uses this approach. The probe sends three sub-requests:
{"validation":"normal","requests":[
{"method":"POST","path":"http://:"},
{"method":"DELETE","path":"/wp/v2/categories/0"},
{"method":"POST","path":"/wp/v2/block-renderer/core/paragraph"}
]}
The first entry is malformed on purpose to trigger the desync. On a vulnerable server, the categories sub-request comes back with the block-renderer’s permission error (block_cannot_read), which shows its request object was evaluated against the wrong handler. On a patched server, the same sub-request is handled correctly and returns rest_term_invalid. The difference is deterministic.
The probe is safe by design. Category id 0 never exists, the request is unauthenticated so no write capability is ever granted, and on the vulnerable path it lands in a read-permission check that never reaches deletion logic.
Remediation
- Update WordPress Core to 6.9.5 or 7.0.2 (or 6.8.6 if you are holding on the 6.8 branch), then confirm the running version under Dashboard > Updates.
- Do not assume the auto-update landed. Forced updates can stall on file-permission or disk issues, and some managed-hosting and Git-deployed setups block them entirely. Verify per site.
- If you cannot patch immediately, block unauthenticated access to the batch endpoint at the WAF or web server, covering both
/wp-json/batch/v1and/?rest_route=/batch/v1. Blocking only one form leaves the site reachable. - Inventory every WordPress instance you run, including staging, microsites, and one-off campaign sites. Unmanaged installs are the ones that miss the update.
- On sites that do not need it, limit the unauthenticated REST API surface.
After patching
Automated scanning for vulnerable instances began shortly after disclosure, and mass exploitation of a CVSS 9.8 pre-auth RCE typically arrives within hours. A patch does not remove an attacker who already dropped a webshell, so any site that sat on an affected version after 17 July should be checked before it is considered clean.
Common signs of post-exploitation on WordPress include newly written PHP files in wp-content/uploads/ or the theme directory, unexpected admin users, modified core files, outbound connections from the web host, and unfamiliar scheduled tasks or must-use plugins. Because the exploit blends into legitimate REST traffic, the more reliable signals are behavioural: a web-facing PHP process spawning a shell, a new admin account, or anomalous outbound traffic. Detecting those requires log collection, correlation, and monitoring.
This analysis is based on the public WordPress security advisory and vendor research disclosed on 17 July 2026. It is provided for defensive purposes and contains no working exploit. If you need help confirming that every WordPress instance you run is patched, or checking whether one was reached before it was, get in touch.
Related Resources
GraphQL Security: Preventing Data Exposure and IDOR
GraphQL APIs commonly expose schemas via introspection and allow unauthorized data access through IDOR. Learn how to test for these vulnerabilities and implement proper authorization controls.
Browser Extensions: The Hidden Security Risk Stealing Your Credentials
Browser extensions frequently become compromised through developer account hijacking, excessive data collection, and supply chain attacks. Learn how to monitor and protect your organization.
Contact us
Let's discuss how we can help protect your business and achieve your security goals.