Back to Blog
Insights

wp2shell (CVE-2026-63030): WordPress Core Pre-Auth RCE

wp2shell (CVE-2026-63030): WordPress Core Pre-Auth RCE

On 17 July 2026, WordPress force-pushed an emergency release. wp2shell (CVE-2026-63030 chained with CVE-2026-60137) is a CVSS 9.8 pre-authentication remote code execution vulnerability in WordPress core, fixed in versions 6.9.5 and 7.0.2. No plugin, no theme, no login: a single anonymous request to the REST API is enough to run SQL against your database, dump the administrator password hash, and, on a default install, reach a shell. It was found by Adam Kues of Searchlight Cyber's Assetnote team. If you run WordPress, this is a patch-now event. Here is exactly what it is, who it hits, and how to defend.

Why this one is different

Most WordPress vulnerabilities live in plugins. wp2shell lives in core (the engine that powers a large share of the web), so a bare, plugin-free install is not a safe install. Three properties make it dangerous:

  • Pre-authentication. The attacker never logs in. Assetnote describes it as having no preconditions and being exploitable by an anonymous user.
  • Core, default config. No add-ons required. If you run an affected version, you're in scope.
  • Two bugs, chained. A REST "batch route confusion" lets a request execute a handler it was never authorized for; that confusion then unlocks a SQL injection that would otherwise be unreachable.

Are you affected?

WordPress versionImpactAction
6.9.0 – 6.9.4Full pre-auth RCE chainUpdate to 6.9.5 now
7.0.0 – 7.0.1Full pre-auth RCE chainUpdate to 7.0.2 now
6.8.0 – 6.8.5SQL injection only (still serious)Update as soon as possible
Below 6.8Not affectedn/a

The vulnerable batch code shipped with WordPress 6.9 in December 2025, so older installs pre-date it. The fixes are 6.9.5 and 7.0.2. WordPress issued automatic updates for this one, but auto-updates are switched off on many managed, staged, or hardened hosts, so check your actual running version rather than assuming.

Bug one: batch route confusion (CVE-2026-63030)

WordPress's REST API has a batch endpoint (POST /wp-json/batch/v1) that lets a client bundle several sub-requests into one call. Each sub-request's HTTP method is constrained by a schema enum that allows POST, PUT, PATCH, DELETE, deliberately not GET.

Internally, WP_REST_Server::serve_batch_request_v1() builds several parallel lists and relies on them staying index-aligned. In the vulnerable versions, one branch breaks that invariant:

foreach ( $requests as $single_request ) {
    if ( is_wp_error( $single_request ) ) {  // e.g. a path that failed to parse
        $has_error    = true;
        $validation[] = $single_request;      // appended to $validation…
        continue;                             // …but NOT to $matches  → arrays desync
    }
    $match     = $this->match_request_to_handler( $single_request );
    $matches[] = $match;                      // only non-error requests land in $matches
}

An error entry lands in $validation but not $matches, so the two lists fall out of step. When the dispatch loop later indexes both with the same counter, every request after the error runs under the next request's handler and permission check. A low-privilege request slips through a gate meant for a privileged one, and the privileged handler executes.

The trigger is any sub-request whose path fails to parse as a URL. The fix, shipped in 6.9.5 / 7.0.2, is a single line that keeps the arrays aligned:

if ( is_wp_error( $single_request ) ) {
    $has_error    = true;
    $matches[]    = $single_request;   // fix: keep $matches aligned with $validation
    $validation[] = $single_request;
    continue;
}

By nesting a batch call inside itself, an attacker uses this desync twice: first to reach the batch handler again, then to smuggle a schema-forbidden GET through to a read handler that carries the injectable parameter.

Bug two: the SQL injection (CVE-2026-60137)

The route confusion delivers a GET request with an author_exclude parameter into the posts query, as a raw string, because the sanitizer that would have forced it to an integer array never ran. WP_Query then does this:

if ( ! empty( $query_vars['author__not_in'] ) ) {
    if ( is_array( $query_vars['author__not_in'] ) ) {                  // only when ARRAY
        $query_vars['author__not_in'] =
            array_unique( array_map( 'absint', $query_vars['author__not_in'] ) );
    }
    $author__not_in = implode( ',', (array) $query_vars['author__not_in'] );
    $where .= " AND {$wpdb->posts}.post_author NOT IN ($author__not_in) ";  // raw interpolation
}

absint() (the integer coercion that would neutralize the input) only runs on the is_array() branch. A string skips it entirely and is interpolated straight into the NOT IN (...) clause, producing a classic time-based blind SQL injection.

From SQL injection to a shell

Assetnote deliberately withheld the direct SQL-injection-to-RCE step. The practical, publicly described completion is straightforward:

  1. Use the blind injection to read wp_users.user_pass, the administrator's bcrypt hash.
  2. Crack that hash offline. This only succeeds against a weak password, which is the real limiter on the full chain.
  3. Log in as the administrator and upload a plugin or theme containing PHP, reaching code execution as the web-server user.

WordPress notes the RCE applies "only when a persistent object cache is not in use." The most plausible reading: a default, database-backed install re-runs the injected query on every request, keeping the blind oracle alive; a persistent object cache (Redis, Memcached) can serve cached results and starve the oracle. Do not treat an object cache as a fix: it may blunt the final jump, but the SQL injection itself remains, and the safe assumption is that any affected version is exploitable.

How to detect exploitation attempts

If you keep request logs or run a WAF, these are the tells:

  • POST to /wp-json/batch/v1 (or /?rest_route=/batch/v1) whose JSON requests[] contains a path that is not a valid REST route (especially an unparseable value) or a nested requests array inside a sub-request body. Legitimate batch traffic never needs either.
  • Batch sub-requests with method: "GET". That method is schema-illegal at the top level, so it only appears through nesting.
  • REST query strings containing author_exclude= with non-numeric content such as ), SLEEP, SELECT, or --.
  • Multi-second latency on anonymous requests to /wp/v2/posts or /batch/v1, the signature of a time-based blind injection.

How to protect your site

In order of priority:

  1. Update to 6.9.5 or 7.0.2 immediately. This is the only real fix. Confirm the version that's actually running, not what your dashboard claims.
  2. If you genuinely can't patch this minute, block the batch endpoint (/wp-json/batch/v1 and /?rest_route=/batch/v1) at your WAF or reverse proxy. Very few sites use it.
  3. Restrict anonymous REST access where your site's functionality allows.
  4. Enforce strong administrator passwords. A strong hash breaks the offline-crack step that the practical RCE path depends on, turning a full compromise into "only" a data-exposure bug.

Frequently Asked Questions

Do I need a plugin installed to be vulnerable?

No. wp2shell is in WordPress core's REST batch API. A clean, plugin-free install on an affected version is exploitable. This is what makes it more serious than the typical plugin CVE.

I use a persistent object cache. Am I safe?

No. WordPress notes the full RCE applies when a persistent object cache is not in use, so a cache may interfere with the final step, but the underlying SQL injection is still present, and cache behaviour is not a security control. Patch to 6.9.5 / 7.0.2 regardless.

The REST API is public by design: should I just disable it?

Blocking the specific /batch/v1 endpoint is a reasonable emergency mitigation, since few sites use it. Disabling the whole REST API is heavier-handed and breaks the block editor, many plugins, and headless setups, so treat it as a temporary measure, not a substitute for patching.

How do I know if I was already exploited?

Check request logs for the detection signatures above, review administrator accounts and recently uploaded plugins/themes for anything you didn't add, and rotate all administrator passwords and secret keys after patching. If you find evidence of the batch/author_exclude patterns landing before your update, treat the site as compromised and rebuild from a known-good backup.


wp2shell is a reminder that "just WordPress core, nothing fancy" is not a security posture. Chained-bug, pre-auth flaws in the platform itself are exactly the class we hunt for in a penetration test, design against in secure web development, and watch for in managed detection. If you're not sure whether your site was exposed in the window before you patched, talk to us. We can help you check.

Get Started

Want a security-first build?

Get a free security review. We'll look at where you stand today and tell you what to fix first, no strings attached.

Talk to an Expert
wp2shell (CVE-2026-63030): WordPress Core Pre-Auth RCE - IKZERO