Sieht Ihr Programm, was Angreifer sehen?

Laden Sie den Offensive Security Benchmark Report 2026 herunter und erfahren Sie, welche Bedrohungen dieses Jahr im Fokus stehen.

No items found.
Schwachstellenwarnungen
-
mins read
-
July 17, 2026

wp2shell: A Pre-Authentication RCE in WordPress Core's REST Batch API

-
- -
wp2shell: A Pre-Authentication RCE in WordPress Core's REST Batch API

WordPress runs about 43% of all websites (W3Techs). In July 2026, WordPress shipped a coordinated security release to close a serious issue: an unauthenticated remote code execution that works against a stock install with no plugins and no special configuration.

Adam Kues of Assetnote / Searchlight Cyber reported the issue and published a public checker at wp2shell.com. The technical details were held back to give administrators time to patch. This post reconstructs the root cause from the released patch, explains why the bug is so easy to reach, and shares the safe detection we built for it.

Technical overview

Component WordPress core REST API batch endpoint (batch/v1)
Class REST batch-route handler confusion leading to remote code execution
Affected WordPress 6.9.0-6.9.4 and 7.0.0-7.0.1
Not affected 6.8.5 and earlier
Fixed in 6.9.5, 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)
CWE CWE-284 Improper Access Control

Exposure risk

The batch API is part of WordPress core, on by default, and reachable without authentication. It does not need pretty permalinks: the endpoint answers on both /wp-json/batch/v1 and the always-available /?rest_route=/batch/v1 form, so installs that turn off rewrite rules are still exposed. Any affected-version site that exposes the REST API, which is almost all of them, is in scope. There is no login page in front of it and no plugin to blame.

Impact assessment

An attacker who reaches the bug gains unauthenticated code execution on the web server. In practice that means full control of the site and its content, access to the database and whatever credentials or personal data it holds, and a way into the surrounding hosting environment. For the large share of WordPress sites that share hosting infrastructure, one compromised instance is rarely the end of the blast radius.

Detection challenges

The exploit looks like ordinary REST traffic. It is a single POST to a legitimate core endpoint with a well-formed JSON body, hard to tell apart 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-based rule. Sites that strip the generator version string, a common hardening step, also remove the one passive signal that would flag an affected version. Any detection that reads the version off the homepage will miss exactly the installs that bothered to harden.

Technical breakdown

The batch API lets a client bundle several REST calls into one request. Core resolves each sub-request to a route and handler, validates them, then dispatches them in a second pass. While it does this it keeps three parallel lists, $requests, $validation, and $matches, and depends on all three staying index-aligned.

In WP_REST_Server::serve_batch_request_v1(), a sub-request that fails early (for example, a path that cannot be parsed as a URL) is recorded as an error and skipped:

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...

}

The error is appended to $validation but not to $matches. From that point on, $matches is one element shorter than $requests and $validation. The dispatch loop then indexes straight into it:

foreach ( $requests as $i => $single_request ) {

    // ...

    $match = $matches[ $i ];   // now 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 and handler that belong to the next sub-request in the batch, including that handler's permission callback. An attacker who controls the order of the sub-requests controls which request object gets evaluated against which permission check. A benign, low-privilege request can pass the permission gate that a privileged handler was meant to enforce, and the privileged handler then runs. Chained to the other sinks reachable through the batch route, that handler confusion is what becomes code execution.

The fix is two lines. 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 in the middle of a 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;

          $validation[] = $single_request;

          continue;

      }

Both changes are identical across the 6.9 and 7.0 branches (6.9.4 to 6.9.5 and 7.0.1 to 7.0.2).

Observed attack chain

  1. Send an unauthenticated POST to /?rest_route=/batch/v1 with "validation":"normal" and an array of sub-requests.
  2. Make the first sub-request fail to parse. A path such as http://: returns a WP_Error before matching, and it is the element that never reaches $matches.
  3. Every following sub-request is now dispatched against the next handler's route and permission callback.
  4. Order the remaining sub-requests so a privileged handler is evaluated against a permission check it would otherwise fail, steering execution into a reachable sink.
  5. The batch endpoint performs an action the caller was never authorized to make, ending in code execution.

Detection

Because the desync is a property of how core dispatches the batch, not of any version string, you can confirm it directly and safely. A crafted batch produces a response that only a vulnerable core returns, and it changes nothing on the target.

The probe sends three sub-requests: a malformed first entry to trigger the desync, a DELETE against /wp/v2/categories/0, and a POST against the block-renderer route.

{"validation":"normal","requests":[

  {"method":"POST","path":"http://:"},

  {"method":"DELETE","path":"/wp/v2/categories/0"},

  {"method":"POST","path":"/wp/v2/block-renderer/core/paragraph"}]}

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 block-renderer handler. On a patched server the same sub-request is handled correctly and returns rest_term_invalid. The difference is deterministic.

The probe is non-destructive by design. Category id 0 never exists, the request is unauthenticated so no write capability is ever granted, and on the vulnerable path the sub-request lands in a read-permission check that never reaches deletion logic. In the lab, firing the probe repeatedly left the category set untouched.

Recommended actions

Immediate:

  • Update WordPress core to 6.9.5 or 7.0.2 (or later). Check that the update actually applied. Auto-updates can stall quietly on file-permission or disk problems.
  • If you cannot update right away, block unauthenticated access to the batch endpoint at the WAF or web server, covering both /wp-json/batch/v1 and /?rest_route=/batch/v1. Blocking only one form leaves the site reachable.

Short term:

  • Add a must-use plugin that rejects anonymous requests to the batch route until the core update is in place.
  • Inventory every WordPress instance you run, including the staging and campaign sites everyone forgets. Those are the ones that miss the update.

Long term:

  • Limit the REST API surface for unauthenticated users on sites that do not need it.
  • Treat "core is on auto-update" as something to verify, not a control to assume.

The structural problem

REST batch APIs exist to save round trips. To deliver that, though, they take on something genuinely awkward: running several independent requests, each with its own permissions, inside a single privileged dispatch. The security of the whole arrangement rests on one quiet assumption, that every request stays tied to its own permission check for the entire cycle. In the affected versions of WordPress Core, that assumption breaks by a single index. 

When the batch handler skips a malformed entry, the array tracking matched handlers drifts out of step with the array of requests, and from that point on each request is authorized against a handler that belongs to someone else in the batch. An operation you were never allowed to perform is suddenly waved through, because the server checked the wrong request's permission.

Detection and tooling

Hadrian detects this across attack surfaces which confirms the desync instead of guessing from a version string. It does not miss version-stripped ones. It is safe to run and changes nothing on the target.

{{related-article}}

wp2shell: A Pre-Authentication RCE in WordPress Core's REST Batch API

{{quote-1}}

,

{{quote-2}}

,

Related articles.

All resources
No items found.

Related articles.

All resources

Schwachstellenwarnungen

CVE-2026-44212 — Stored XSS in PrestaShop Back-Office via RFC 5321 Quoted-String Email

CVE-2026-44212 — Stored XSS in PrestaShop Back-Office via RFC 5321 Quoted-String Email

Schwachstellenwarnungen

Next.js WebSocket SSRF: Unauthenticated Access to Internal Resources: CVE-2026-44578

Next.js WebSocket SSRF: Unauthenticated Access to Internal Resources: CVE-2026-44578

Schwachstellenwarnungen

CVE-2026-23918: Apache HTTP Server Double-Free RCE in HTTP/2 Implementation

CVE-2026-23918: Apache HTTP Server Double-Free RCE in HTTP/2 Implementation

get a 15 min demo

Start your journey today

Hadrian’s end-to-end offensive security platform sets up in minutes, operates autonomously, and provides easy-to-action insights.

What you will learn

  • Monitor assets and config changes

  • Understand asset context

  • Identify risks, reduce false positives

  • Prioritize high-impact risks

  • Streamline remediation

The Hadrian platform displayed on a tablet.
No items found.