WordPress wp2shell RCE: CVE-2026-63030 and CVE-2026-60137
On July 17, 2026, WordPress pushed an emergency security update. WordPress 7.0.2, 6.9.5, and 6.8.6 fixed two core vulnerabilities: CVE-2026-63030 and CVE-2026-60137. Our team at Element Security analyzed the patched code to understand the vulnerabilities, protect our customers, and develop an exploit module for our External Security platform. We identified both root causes and investigated multiple paths to pre-authentication remote code execution.
As a result, the module enables our platform to detect vulnerable WordPress installations and validate exposure to the attack chain. This post documents what we found, how the chain works, and what defenders need to know.
How We Started: Diffing 7.0.1 and 7.0.2
After the patch dropped, we pulled both WordPress versions and ran a diff. Three files had security-relevant changes, and they told the entire story.
wp-includes/class-wp-query.php: WordPress only sanitized the author__not_in parameter to integers when code passed it as an array. However, a scalar string skipped absint() entirely and reached the SQL NOT IN (...) clause raw. The fix wraps all input through wp_parse_id_list() regardless of type.
wp-includes/rest-api/class-wp-rest-server.php: Inside serve_batch_request_v1(), a loop builds two parallel arrays: $matches (route handler bindings) and $validation (validation results). When a sub-request failed to parse, WordPress pushed it to $validation but not to $matches. This one-line omission desynchronized the arrays and caused WordPress to dispatch later sub-requests against the wrong route handler.
wp-includes/rest-api.php: WordPress added a re-entrancy guard to rest_api_loaded() to prevent a nested batch from triggering a fresh top-level REST dispatch cycle.
The diff made it clear: this is a two-bug chain. The route confusion (CVE-2026-63030) makes the SQL injection (CVE-2026-60137) reachable without authentication. Therefore, neither bug alone delivers the full impact.
Bug #1: The Route Confusion
WordPress’s /batch/v1 endpoint lets you bundle multiple REST API calls into a single HTTP request. WordPress validates each sub-request, matches it to a route handler, and then dispatches it in sequence. The key insight is that validation and dispatch happen in two separate passes over the request list.
In the first pass, WordPress builds $matches[] and $validation[], with one entry per sub-request. When a malformed path fails wp_parse_url(), WordPress adds a WP_Error to $validation. The loop then continues without adding an entry to $matches[], so the two arrays fall out of alignment.
In the second pass, WordPress iterates by numeric index. Sub-request $i runs with $matches[$i]. Because the malformed entry shifts the arrays, WordPress dispatches request B through request C’s route handler, permission check, and schema. The callback then processes request B’s raw, unsanitized parameters even though its own validation never ran.
In practice, using http:/// as the malformed path reliably triggers the parse failure on PHP 8+ (which WordPress 7.x targets). It is the primer that sets up the desync.
Bug #2: The SQL Injection
The REST API parameter author_exclude maps to WP_Query‘s internal author__not_in. Under normal conditions, the posts controller’s schema validates author_exclude as an array of integers. Therefore, an attacker cannot reach the underlying WP_Query bug through the REST API alone. The issue becomes exploitable only when another flaw breaks that validation guarantee.
The route confusion provides that missing flaw. As a result, a crafted scalar string in author_exclude sails past the confused dispatch, reaches WP_Query unsanitized, skips the is_array() gate on absint(), and lands in a raw NOT IN (...) SQL clause. The result is pre-auth SQL injection.
Our Exploit Research: Two Carriers, Two Techniques
After confirming the SQL injection, we explored how to reach RCE without relying on MySQL misconfigurations, file-write privileges, or hash cracking. Many early public discussions assumed those approaches. Instead, we developed the following techniques.
The Widgets Carrier: UNION-Based Injection
We discovered that the /wp/v2/widgets endpoint, when used as the inner carrier in the batch confusion, reflects full UNION SELECT output back through the JSON response. This gives the attacker both read and synthetic write capability. It does not use SQL INSERT. Instead, it returns crafted rows that WordPress processes as real internal objects.
The UNION output lets us construct entire fake wp_posts rows with controlled post_content, post_type, post_status, and parent relationships. Consequently, WordPress’s downstream processing treats these as legitimate objects and triggers the application-level side effects required for the RCE chain.
The Categories Carrier: Boolean-Blind Extraction
We also found that /wp/v2/categories works as a lighter carrier. The injection point is the same (author_exclude), but the oracle is the X-WP-Total response header. This count of matching posts shifts depending on whether an injected OR IF(...) condition is true or false.
Using binary search over character ordinals, this approach extracts database values at roughly 7 to 8 requests per character. A 34-character WordPress phpass hash needs about 250 requests. It is slower than UNION, but the payload is tiny: 0) OR 1=1-- -. As a result, WAFs are far less likely to catch it.
However, in our testing against over 400 targets, the categories carrier confirmed vulnerable hosts that the UNION variant couldn’t reach because WAF signature rules blocked the heavier UNION SELECT keywords.
From SQLi to Admin: The oEmbed Cache Poisoning Chain
This is where it gets interesting. Early public speculation focused on MySQL LOAD_FILE/INTO OUTFILE for writing a webshell, or on cracking extracted password hashes. Both approaches are unreliable: secure_file_priv blocks file operations on most production MySQL installations, and strong admin passwords won’t crack.
Instead, we developed a path that uses the UNION injection to build a graph of interlinked WordPress objects. This graph satisfies the internal preconditions for user creation through the REST API. The technique does not require database writes via SQL.
How the Object Chain Creates an Administrator
- Schema discovery: We query
INFORMATION_SCHEMAthrough the UNION injection to find the WordPress table prefix. This is necessary because not every installation uses the defaultwp_. - Admin identification: We read the
usersandusermetatables to find an existing user with theadministratorcapability. We need their user ID for content reassignment during cleanup. - oEmbed cache seeding: We return synthetic posts containing
shortcodes via UNION. When WordPress renders these, it processes the embeds and creates
oembed_cachepost entries. Thepost_nameof each cache entry is the MD5 hash of the embed URL plus dimensions, both of which we control and can predict. - Object graph construction: Using the known oEmbed cache post IDs, we inject a set of interlinked WordPress objects through a second UNION batch: a
customize_changeset, anav_menu_item, and arequestpost. These objects have specific parent/child relationships and status values that satisfy the internal permission and state checks WordPress performs when processing user creation. - Pre-auth admin creation: We append a
POST /wp/v2/userssub-request to the same confused nested batch, requesting a new administrator account. Because the object graph satisfies WordPress’s internal preconditions and the route confusion runs the permission check against the wrong context, WordPress creates the user. - Plugin upload and shell: We log in with the new admin credentials, upload a minimal PHP plugin through the standard plugin installer, and execute commands through it.
- Cleanup: The module deletes the temporary admin account, reassigns its content to the original admin, and removes the plugin directory. This minimizes forensic traces in the WordPress dashboard.
The full chain requires only a small number of HTTP round trips. We implemented it as an internal exploit module for the Element Security platform to detect and validate vulnerable WordPress installations.

Impact and Preconditions
The full RCE chain works on a default WordPress installation:
- No authentication, cookies, or user interaction required.
- No vulnerable third-party plugins required. Both bugs are in WordPress core.
- No MySQL misconfiguration required. The chain does not use
LOAD_FILE,INTO OUTFILE, or any file privilege. - No hash cracking required.
Cloudflare notes that the RCE path works when a persistent object cache (Redis, Memcached) is not in use. This is the default for most WordPress installations. A persistent cache may disrupt this specific chain but is not a security fix.
The plugin-upload step requires the site to permit filesystem modifications and make wp-content/plugins/ writable. Even where the site blocks that final step, the SQL injection and admin account creation remain serious impacts.
Detection
Detection is harder than usual because nested JSON request bodies contain the entire attack. Standard access logs only capture the outer URL (POST /wp-json/batch/v1).
Network and WAF Indicators
POSTrequests to/wp-json/batch/v1or/?rest_route=/batch/v1with nestedrequestsarrays.- Intentionally malformed sub-request paths (e.g.,
http:///) followed by valid REST routes. - The
author_excludeparameter containing SQL tokens:UNION,SELECT,OR IF(,SUBSTRING,ORD(. - An unusual mix of
/wp/v2/widgets,/wp/v2/categories,/wp/v2/posts, and/wp/v2/usersin a single batch. - A burst of batch requests followed by
wp-login.phpauthentication and plugin upload activity from the same IP.
WAF rules must parse the JSON body and inspect nested sub-request parameters. The outer HTTP request looks normal; the payload is entirely inside the JSON. Remember to cover both /wp-json/batch/v1 and ?rest_route=/batch/v1.
WordPress and Host Indicators
- New administrator accounts with no corresponding legitimate activity.
- Recently installed plugins with generic or randomized names under
wp-content/plugins/. - Closely timed
oembed_cache,customize_changeset,nav_menu_item, andrequestpost objects. - Changes in the
usersandusermetatables during the affected window.
A thorough attacker will delete the temporary account and plugin after exploitation. Correlate database history, filesystem telemetry, WAF logs, and backups. Do not rely solely on the WordPress dashboard.
Remediation
Update immediately:
- WordPress 7.0.0 through 7.0.1: update to 7.0.2
- WordPress 6.9.0 through 6.9.4: update to 6.9.5
- WordPress 6.8.0 through 6.8.5: update to 6.8.6 (fixes CVE-2026-60137; this branch doesn’t have CVE-2026-63030)
If you can’t patch immediately:
- Block unauthenticated access to
/batch/v1at the WAF or edge. Cover both/wp-json/batch/v1and?rest_route=/batch/v1. - Enable managed WAF rules for both CVEs.
- Restrict plugin installation and filesystem write permissions where operationally feasible.
- Preserve logs before making changes.
If you suspect compromise, rotate all WordPress administrator, database, hosting, and application credentials after validating or rebuilding the host from a trusted state.
FAQ
What is CVE-2026-63030?
A route-confusion vulnerability in the WordPress REST API batch endpoint. When chained with CVE-2026-60137 (a SQL injection in WP_Query::author__not_in), it enables unauthenticated remote code execution.
Is this one vulnerability or two?
Two. CVE-2026-63030 is the route confusion that bypasses parameter validation. CVE-2026-60137 is the SQL injection that the bypass exposes. Neither delivers the full impact alone.
Does it require authentication, a plugin, or MySQL misconfiguration?
No to all three. The chain works pre-auth against WordPress core on a default installation with standard MySQL settings.
Does a persistent object cache (Redis/Memcached) prevent exploitation?
According to Cloudflare, the full RCE path works when a persistent object cache is not in use. A persistent cache may disrupt this specific chain, but it does not fix the underlying vulnerabilities. Update WordPress regardless.
References
- WordPress advisory for CVE-2026-63030
- WordPress advisory for CVE-2026-60137
- WordPress 7.0.2 security release
- NVD entry for CVE-2026-63030
- Cloudflare WordPress vulnerability analysis and WAF rules
Element Security provides continuous external attack surface management and automated vulnerability detection. Our platform detected and verified exposure to this WordPress attack chain across customer environments after disclosure. Learn more at element.security.