WP2Shell diagram showing a REST batch route shift, SQL injection, and CleanTalk blocking the request
WP2Shell shifts the REST route index, reaches the vulnerable SQL query, and is blocked at the request layer before WordPress processes the exploit.

WP2Shell is a critical, unauthenticated remote code execution chain in WordPress core. It combines CVE-2026-63030, a REST API batch-route confusion flaw, with CVE-2026-60137, SQL injection in WP_Query. The full chain affects WordPress 6.9.0 through 6.9.4 and 7.0.0 through 7.0.1. The fixed releases are WordPress 6.9.5 and 7.0.2.

The first defect separates route validation from the handler WordPress eventually executes. The second accepts a scalar where WP_Query expects an array of author IDs, allowing SQL text to reach an unparameterized NOT IN (...) clause. Attackers can combine those behaviors to manipulate database results, create a rogue administrator, and reach PHP execution without an existing account or a vulnerable plugin.

WordPress released the security updates on July 17, 2026 and enabled forced updates. CISA added both vulnerabilities to the Known Exploited Vulnerabilities catalog four days later. This analysis covers the affected versions, the vulnerable PHP code, the public attack path, indicators of compromise, remediation steps, and the request layer where CleanTalk Security can interrupt the chain.

Immediate action: update WordPress core first. Install WordPress 6.8.6, 6.9.5, 7.0.2, or a newer release for the branch in use. A web application firewall is an important compensating control, but it does not replace the core patch. Sites that were exposed before patching should also be checked for compromise.

WP2Shell quick facts

  • Attack class: unauthenticated WordPress core remote code execution.
  • Authentication required: none.
  • Plugins required: none. The vulnerable code is in WordPress core.
  • Full RCE chain: WordPress 6.9.0 through 6.9.4 and 7.0.0 through 7.0.1.
  • SQL injection component: WordPress 6.8.0 through 6.8.5 is also affected by CVE-2026-60137.
  • Fixed releases: WordPress 6.8.6, 6.9.5, and 7.0.2.
  • Exploitation status: both CVEs are listed in CISA KEV.

Jump to the answer: affected versions, active exploitation, how to check a site, attack chain, indicators of compromise, CleanTalk protection, recovery, and FAQ.

WP2Shell affected versions and fixed releases

ComponentRole in the chainAffected versionsFixed versions
CVE-2026-63030REST batch route confusion. Critical, CVSS 9.8.WordPress 6.9.0 through 6.9.4 and 7.0.0 through 7.0.16.9.5 and 7.0.2
CVE-2026-60137SQL injection in WP_Query::author__not_in. Moderate in the GitHub advisory, CNA CVSS 5.9.WordPress 6.8.0 through 6.8.5, 6.9.0 through 6.9.4, and 7.0.0 through 7.0.16.8.6, 6.9.5, and 7.0.2

The published ratings require context. The WordPress release describes one critical and one high-severity issue. The GitHub advisory currently labels CVE-2026-60137 Moderate, and the NVD record shows a CNA CVSS 3.1 score of 5.9. The full RCE impact belongs to the chain with CVE-2026-63030, whose CNA score is 9.8 and Critical.

The version split matters. WordPress 6.8 contained the SQL injection bug but not the batch route confusion needed for the published pre-authentication RCE chain. Versions before 6.8 are not affected by these two vulnerabilities. The authoritative version matrix appears in the WordPress 7.0.2 security release and the two GitHub advisories for CVE-2026-63030 and CVE-2026-60137.

Official WordPress 7.0.2 security updates listing SQL injection and REST batch route confusion leading to RCE
The official WordPress 7.0.2 release lists the SQL injection and REST batch route-confusion issue leading to remote code execution.

Is WP2Shell being exploited? Current data as of July 30, 2026

The strongest official signal is CISA inclusion. As of July 30, both CVEs are in the Known Exploited Vulnerabilities catalog.

The NVD record for CVE-2026-63030 lists active exploitation, automatable attack behavior, total technical impact, and a July 24 remediation deadline. The NVD record for CVE-2026-60137 lists active exploitation and an August 4 remediation deadline. NHS England also rated the threat high, citing a public proof of concept and reported exploitation in its CC-4815 cyber alert.

Independent cloud telemetry provides a useful exposure estimate. Wiz Research reported that at disclosure time, 60% of organizations using WordPress in its cloud dataset had at least one vulnerable instance and 25% exposed a vulnerable server to the Internet. Within 24 hours of forced updates, those figures fell to 50% and 10%. These percentages describe assets visible in the Wiz cloud dataset. They should not be treated as a census of the entire public WordPress ecosystem.

Wiz exposure statistics showing vulnerable WordPress instances before and after forced updates
Wiz measured a sharp fall in vulnerable cloud instances within 24 hours of forced WordPress updates. The figures describe the Wiz cloud dataset, not every WordPress site.

How to check whether a WordPress site is vulnerable

Checking exposure does not require sending an exploit request. The installed WordPress version is enough to determine whether the published WP2Shell code path is present.

  1. In the dashboard, open Dashboard > Updates or Tools > Site Health > Info > WordPress and record the exact core version.
  2. With WP-CLI access, run the command below from the WordPress directory.
  3. Compare the result with the affected version table. WordPress 6.9.0 through 6.9.4 and 7.0.0 through 7.0.1 are vulnerable to the full RCE chain.
  4. If the site was updated only after July 17, review the indicators of compromise. Patching closes the flaw but does not remove access gained earlier.
wp core version

A result of 6.8.6, 6.9.5, 7.0.2, or any later supported release is patched for the corresponding branch. WordPress 6.8.0 through 6.8.5 does not contain the complete pre-authentication RCE chain, but it remains vulnerable to the CVE-2026-60137 SQL injection component and should be updated.

CVE-2026-63030: REST API batch route confusion

The batch endpoint prepares every subrequest in one pass and executes the prepared entries in another. WordPress used two parallel arrays. $validation[$i] stored the validation result for subrequest $i. $matches[$i] stored the route handler selected for the same subrequest. That design is safe only while both arrays receive exactly one entry for every input.

In the vulnerable branch, a malformed subrequest produced WP_Error. The error was appended to $validation, but nothing was appended to $matches before continue. The excerpt below shows the relevant control flow. The omitted branch checks the matched route and parameters before writing the final validation result.

if ( is_wp_error( $single_request ) ) {
    $has_error    = true;
    $validation[] = $single_request;
    continue;
}

$match     = $this->match_request_to_handler( $single_request );
$matches[] = $match;
$error     = null;

// Route and parameter checks populate $error.

if ( $error ) {
    $has_error    = true;
    $validation[] = $error;
} else {
    $validation[] = true;
}

After one malformed item, the index relationship is broken. If request 0 is malformed, $validation[1] still describes request 1, but $matches[1] now describes request 2. During execution, request 1 can therefore pass the checks associated with its own, less restrictive route and then be dispatched through the handler selected for request 2. This is an interpretation conflict at the routing layer. The request body is interpreted under a schema that did not validate it.

Input positionValidation entryHandler entry after the bug
0: malformed requestvalidation[0] contains the errorNo matches[0] entry is added
1: route with looser schemavalidation[1] describes request 1matches[0] points to request 1
2: target routevalidation[2] describes request 2matches[1] points to request 2
Execution at index 1Checks for request 1 are acceptedHandler for request 2 is selected

The 7.0.2 patch restores the invariant with one line. Even an invalid request now consumes a position in both arrays:

if ( is_wp_error( $single_request ) ) {
    $has_error    = true;
    $matches[]    = $single_request;
    $validation[] = $single_request;
    continue;
}

WordPress also added re-entrancy guards around REST dispatch. Those checks prevent an already active REST request from starting another top-level REST dispatch inside the same PHP execution. That closes the recursive dispatch step used by public WP2Shell tooling.

CVE-2026-60137: SQL injection in WP_Query

The posts REST endpoint exposes the public author_exclude parameter and maps it to the internal WP_Query variable author__not_in. Under the correct REST schema, the value is an array of integer author IDs. Route confusion breaks that assumption and allows a scalar string to reach WP_Query.

The vulnerable code sanitized values only when PHP already considered the input an array. A scalar skipped absint(), was cast to a one-element array, joined without quoting, and interpolated directly inside a SQL NOT IN (...) clause:

if ( ! empty( $query_vars['author__not_in'] ) ) {
    if ( is_array( $query_vars['author__not_in'] ) ) {
        $query_vars['author__not_in'] = array_unique(
            array_map( 'absint', $query_vars['author__not_in'] )
        );
        sort( $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) ";
}

For a normal value such as [7, 12], the branch converts both elements to integers. For a confused scalar such as "7, attacker-controlled SQL", is_array() is false. The cast to (array) does not sanitize the string. It only wraps the string so that implode() can return it unchanged. The SQL parser, not WordPress, then decides what the extra tokens mean.

The fixed code normalizes every input through wp_parse_id_list(). Only integer IDs survive before the list is inserted into SQL:

$author__not_in_id_list = wp_parse_id_list(
    $query_vars['author__not_in']
);

if ( count( $author__not_in_id_list ) > 0 ) {
    sort( $author__not_in_id_list );
    $where .= sprintf(
        " AND {$wpdb->posts}.post_author NOT IN (%s) ",
        implode( ',', $author__not_in_id_list )
    );
    $query_vars['author__not_in'] = $author__not_in_id_list;
}

The correction restores a strict data type before SQL construction. CleanTalk’s guide to preventing SQL injection in WordPress explains the broader pattern, including typed placeholders and prepared queries for dynamic values.

How the WP2Shell exploit chain works

The public chain nests one batch inside another. The outer /batch/v1 schema normally permits only POST, PUT, PATCH, and DELETE subrequests, while the vulnerable posts collection query is a GET.

The outer route shift sends an inner requests body to the batch handler without validating it under the batch schema. The inner route shift then validates author_exclude against a route that does not recognize the parameter and executes it through GET /wp/v2/posts.

The schematic request below separates the two index shifts. The outer shift bypasses method validation for the inner batch. The inner shift moves the scalar author_exclude value into the posts collection handler.

POST /wp-json/batch/v1
Content-Type: application/json

{
  "requests": [
    { "method": "POST", "path": "<malformed path>" },
    {
      "method": "POST",
      "path": "/wp/v2/posts",
      "body": {
        "requests": [
          { "method": "GET", "path": "<malformed path>" },
          {
            "method": "DELETE",
            "path": "/wp/v2/posts/1",
            "body": {
              "author_exclude": "<attacker-controlled scalar>"
            }
          },
          { "method": "GET", "path": "/wp/v2/posts" }
        ]
      }
    },
    { "method": "POST", "path": "/batch/v1" }
  ]
}
  1. The first malformed path in the outer batch adds a validation error but no matching handler entry.
  2. Because $matches is shifted, the outer request at index 1 is executed by the handler matched for index 2, /batch/v1.
  3. The inner requests body reaches the batch handler without the outer schema rejecting its GET methods.
  4. A second malformed path repeats the array shift inside the inner batch.
  5. The scalar author_exclude value is accepted under DELETE /wp/v2/posts/1, which does not recognize that parameter, then executed by the shifted GET /wp/v2/posts handler.
  6. WP_Query maps the scalar to author__not_in. The resulting SQL response can be shaped as rows that WordPress hydrates into WP_Post objects.

How read-only SQL injection becomes administrator access

A common assumption is that a SELECT injection can only disclose data. WP2Shell demonstrates why that assumption is unsafe inside a stateful content management system. The exploit does not need to crack a stored password hash or issue a direct INSERT. It uses crafted query results to poison WordPress objects that later participate in legitimate write operations. The Searchlight Cyber technical analysis documents the complete transition.

The published RCE path uses two HTTP requests. The first makes WordPress create three real oembed_cache rows. The second feeds six forged WP_Post objects into the embed, changeset, parent-cycle, and dynamic-hook code paths. Those objects convert a read primitive into authenticated state changes in the following order.

  1. A UNION-shaped result makes the posts query return rows chosen by the attacker. WordPress hydrates those rows as WP_Post objects and places them in the per-request object cache.
  2. Crafted post content triggers local handling. WordPress writes real oembed_cache rows while still trusting the poisoned post objects in memory.
  3. The attacker builds a small object graph that includes forged customize_changeset and request posts with cyclic parent relationships.
  4. WordPress repairs the detected parent cycle through wp_update_post(). That is a legitimate write primitive fed by attacker-controlled values recovered from the poisoned cache.
  5. Applying the forged customization changeset temporarily calls wp_set_current_user() for an administrator ID represented in the crafted state.
  6. A forged status or type causes the dynamic parse_request hook to run while that administrator context is active. The pending REST batch is dispatched again inside the same PHP request.
  7. The replayed POST /wp/v2/users now passes capability checks and creates a rogue administrator.
  8. With dashboard access, the attacker can use standard plugin installation or upload behavior to place executable PHP. Code then runs with the web server account permissions.

The chain creates several independent detection opportunities. It depends on nested batch requests, unusual post-query values, object-cache side effects, administrator creation, and plugin installation. Blocking the initial request prevents every downstream stage. Detection of any later stage should trigger incident response because persistent access may already exist.

WP2Shell indicators of compromise and attack activity

Wiz reported malicious plugin uploads, user enumeration, local file inclusion attempts, administrator access, and high-volume scanning soon after disclosure. Bitdefender incident telemetry observed three scripted attempts against one environment. The first two created rogue administrator users with a w2s_ naming pattern and stalled. The third progressed to malicious plugin and webshell uploads. Names are easy to change, so behavior is a stronger signal than a single username prefix or filename.

SignalWhy it matters
Nested or repeated POST requests to /wp-json/batch/v1Matches the route-confusion delivery mechanism
HTTP 207 Multi-Status responses around unusual batch trafficNormal for batch results, but suspicious when paired with malformed subrequests and anonymous clients
Unexpected administrator creationIndicates the privilege boundary may already be crossed
New plugin directories or PHP files without a recorded maintenance actionPossible transition from admin access to code execution
Requests combining posts queries with abnormal author_exclude valuesTargets the vulnerable SQL path
Follow-on access to plugin upload, theme editing, or user-management endpointsShows attackers using newly acquired administrative capabilities

Does CleanTalk Security protect against WP2Shell?

The Security & Malware scan by CleanTalk plugin already provides request-layer protection against the SQL injection and exploit patterns used by WP2Shell when the Security Firewall, Web Application Firewall, SQL-injection check, and exploit check are enabled and the plugin is current. Its file scanning and cloud malware detection also help identify suspicious PHP files and malicious plugins left after a successful compromise.

The WAF inspects GET and POST traffic, including REST request bodies. It can reject the harmful request before WordPress reaches the vulnerable batch dispatcher and WP_Query code.

This placement matters because WP2Shell begins before authentication. A dashboard-only scanner cannot prevent the first request. CleanTalk evaluates inbound traffic before WordPress dispatches the vulnerable REST route, while the nested batch structure, SQL injection tokens, and exploit signatures remain visible. The official CleanTalk WAF documentation describes protection for dynamic resources, SQL injection attempts, malicious uploads, and exploit traffic.

CleanTalk Security settings with Security Firewall, WAF, SQL-injection check, and exploit checks enabled
CleanTalk Security combines the Security Firewall, WAF, SQL-injection inspection, and exploit detection to stop malicious requests before they reach vulnerable WordPress code.

Defense in depth: CleanTalk Security can block WP2Shell request patterns and follow-on malicious uploads, but administrators must still install the corrected WordPress core version. A WAF protects traffic. The patch restores the broken invariants in the application itself.

How to detect and recover from WP2Shell

  • Confirm the exact WordPress core version from the filesystem or Site Health. Do not rely only on an update notification that may be stale.
  • Review web access logs for anonymous POST traffic to /wp-json/batch/v1, especially requests followed by HTTP 207 responses and access to user or plugin endpoints.
  • Audit all administrator accounts, creation timestamps, email addresses, application passwords, and active sessions. Remove unknown accounts only after preserving evidence.
  • Compare plugin and theme directories against known-good deployment artifacts. Inspect recently modified PHP files and uploads that should not contain PHP.
  • Run a public surface check with the CleanTalk Website Malware Scanner to look for injected scripts, hidden links, unsafe external calls, and other browser-visible compromise signals. Treat this as an additional check, not a replacement for server-side forensics.
  • Rotate WordPress salts, administrator credentials, database credentials, hosting control-panel credentials, and deployment secrets after confirmed compromise.
  • Reinstall WordPress core and all extensions from trusted packages. A version update alone does not remove a webshell or stolen credential.
  • Keep CleanTalk Security current, then enable the Security Firewall, WAF, SQL-injection check, exploit detection, and file scanning. Review Security Log entries for blocked and allowed follow-on activity.
  • Preserve server, WAF, authentication, database, and file-integrity logs before cleanup. Successful WP2Shell exploitation can create several independent persistence paths.

WP2Shell frequently asked questions

What is WP2Shell?

WP2Shell is the name given to a WordPress core exploit chain that combines REST API route confusion with SQL injection. An unauthenticated attacker can use the chain to create an administrator account and execute PHP through normal WordPress administration features.

Which WordPress versions are vulnerable to WP2Shell?

The full WP2Shell RCE chain affects WordPress 6.9.0 through 6.9.4 and 7.0.0 through 7.0.1. WordPress 6.8.0 through 6.8.5 contains the SQL injection component only. The fixed releases are WordPress 6.8.6, 6.9.5, and 7.0.2.

How can a site owner check for WP2Shell exposure?

Check the exact WordPress core version in Dashboard > Updates, Site Health, or with wp core version. Compare it with the affected version ranges above. If the site was vulnerable before it was patched, review access logs, administrator accounts, plugins, and recently modified PHP files for compromise.

Is WP2Shell being actively exploited?

Yes. CISA lists both CVE-2026-63030 and CVE-2026-60137 in the Known Exploited Vulnerabilities catalog. Public exploit tooling and multiple security vendors have also reported scanning, rogue administrator creation, malicious plugin uploads, and webshell deployment.

Is WordPress 6.8 vulnerable to full WP2Shell RCE?

WordPress 6.8.0 through 6.8.5 contains CVE-2026-60137, the SQL injection component. The published pre-authentication WP2Shell chain also requires the REST batch route confusion introduced in WordPress 6.9. WordPress 6.8 sites should still update to 6.8.6 because the SQL injection defect is independently security relevant.

Does a persistent object cache fix WP2Shell?

No. The demonstrated chain relies on WordPress object behavior within a single PHP request. Changing Redis, Memcached, or another persistent cache setting is not a reliable remediation. The core patches and request-layer protection are the relevant controls.

Is updating enough after a confirmed compromise?

No. Updating prevents the vulnerable code path from being used again. It does not remove a rogue administrator, a malicious plugin, a webshell, stolen credentials, scheduled tasks, or modified files. Confirmed exploitation requires a full incident-response and recovery process.

Does CleanTalk Security protect against WP2Shell?

Yes. An up-to-date CleanTalk Security installation can block the SQL injection and exploit patterns used by WP2Shell when its Security Firewall, WAF, SQL-injection check, and exploit detection are enabled. File scanning also helps find suspicious follow-on files. WordPress core must still be updated because the plugin is a defense layer, not the vendor patch.

Can a WAF replace the WordPress patch?

No. CleanTalk Security provides a request-layer control that blocks malicious SQL injection, exploit patterns, and follow-on uploads. The WordPress update remains mandatory because it fixes the route-index invariant, normalizes IDs before SQL construction, and blocks nested REST re-entry.

Remediation priorities

WP2Shell is a composition failure. The route bug is not SQL injection by itself, and the query bug is not pre-authentication RCE by itself. The batch code assumes that its handler and validation arrays remain aligned. WP_Query assumes that REST processing has already enforced an integer array. When the first assumption fails, the second becomes exploitable. WordPress object behavior then converts the read primitive into an authenticated write path.

Sites running an affected branch should update immediately, inspect for the behavioral indicators above, and treat any confirmed rogue administrator or plugin upload as a full compromise. CleanTalk Security should remain enabled as a request-layer control against this threat class, with the Security Firewall, WAF, SQL-injection check, and exploit detection active.

Sources and references

WP2Shell WordPress RCE Explained: CVE-2026-63030 and CVE-2026-60137

Dmitrii I

Pentester with 5 years of hands-on experience securing WordPress and web applications, holding OSWE, OSEP, OSCP, and OSWP certifications. Author of 450 published CVEs, including 35 disclosed within the last month. Specializes in discovering and validating high-impact vulnerabilities in WordPress plugins/themes / Custom WEB applications and delivering actionable remediation guidance to harden production sites.

Visit Author's Website

See all posts by dmitrii-ignatyev

Leave a Reply

Your email address will not be published. Required fields are marked *