WordPress 7.0.2: Unauthenticated RCE in the REST API, Patch Now
WordPress shipped 6.9.5 and 7.0.2 on July 17 and enabled forced auto-updates, closing a pre-authentication remote code execution in core. Not a plugin. A default install with zero plugins is exploitable by an anonymous HTTP request, no login, no user interaction, no preconditions. The researcher who found it published under the name wp2shell, and it is exactly as bad as that name suggests.
WordPress is treating this as its highest-severity class of issue and force-pushing updates. Cloudflare deployed WAF rules at 17:03 UTC the same day, and Wordfence shipped firewall coverage to premium tiers immediately. If you run WordPress anywhere, and especially if you host it for other people, this is a tonight problem, not a this-week one.
What It Is
Adam Kues at Assetnote (Searchlight Cyber's attack surface arm) found the flaw and reported it through WordPress's HackerOne program. His writeup, named wp2shell, states the attack "has no preconditions and can be exploited by an anonymous user." Searchlight is withholding full technical detail for now and has put up a self-check tool at wp2shell.com instead.
There are two related bugs, and they chain:
The RCE. Critical. WordPress core is vulnerable via the REST API batch endpoint at /wp-json/batch/v1. Wordfence describes the mechanism as a route/validation desynchronization: a validated sub-request gets dispatched to an unintended callback, bypassing the allow_batch restriction, with attacker-controlled parameters and the target route's input sanitization bypassed. Chained with the SQL injection below, that becomes code execution on the server. Affects 6.9.0 through 6.9.4 and 7.0.0 through 7.0.1.
The SQL injection. High. Unauthenticated SQLi via the author__not_in parameter, from insufficient escaping and a query that wasn't properly prepared. An unauthenticated attacker appends SQL to an existing query to extract data. Reported by Tin Pham (TF1T), Trong Pham (dtro), and haongo. Affects 6.8 through 7.0.1.
WordPress's own release post calls Kues's finding "a REST API batch-route confusion and SQL injection issue leading to Remote Code Execution," and the 7.0.2 version page lists the three files the fix touched: wp-includes/rest-api/class-wp-rest-server.php, wp-includes/class-wp-query.php, and wp-includes/rest-api.php.
⚠️ The batch endpoint itself is not new. WordPress has shipped it since 5.6 in November 2020 and documented the request format publicly the whole time. Nothing published so far explains what changed in 6.9 to open the hole, which is worth remembering when you read "it only affects 6.9+": that's the vulnerable window, not evidence the endpoint is exotic. It's a documented, long-standing feature that a lot of legitimate code uses, which matters for the mitigations below.
The No-CVE Problem
⚠️ This is the part that will bite fleets quietly. As of July 18, neither advisory carries a CVE ID or a CVSS score, and no CVE record exists. (Wordfence Intelligence has assigned its own tracking IDs and a 9.8 rating internally, and some outlets are citing provisional CVE-2026-63030 / CVE-2026-60137 numbers, but there is no published CVE record yet.)
The practical consequences:
- CVE-keyed scanners and inventory tools will not flag this. If your patch-management or vuln-scanning process keys on CVE IDs, this vulnerability is invisible to it right now.
- CISA cannot add it to the KEV catalog without a CVE, so it won't show up there either.
- You have to track it by version number, manually. That's the whole detection strategy until a CVE lands.
Do not wait for your scanner to tell you. It won't.
Who Is Actually Affected
| Your version | SQLi | RCE (wp2shell) | Fixed in |
|---|---|---|---|
| Below 6.8 | Not affected | Not affected | Nothing to do |
| 6.8.0 - 6.8.5 | Affected | Not affected | 6.8.6 |
| 6.9.0 - 6.9.4 | Affected | Affected | 6.9.5 |
| 7.0.0 - 7.0.1 | Affected | Affected | 7.0.2 |
| 7.1 Beta | Affected | Affected | 7.1 Beta 2 |
⚠️ The two bugs have different floors, and people will get this wrong. 6.8.6 fixes the SQL injection only, because the RCE code path doesn't exist in the 6.8 branch. The RCE was introduced in 6.9 (released December 2, 2025), so every site vulnerable to the critical is running a release less than eight months old. The odd corollary: being behind on WordPress, on anything below 6.8, is the one state that's clean here. Not a lesson to generalize from, but useful for triage.
Searchlight cites the ~500 million-plus WordPress sites figure, but that's the total install base, not the vulnerable population. Only 6.9+ has the RCE. Neither advisory says how many sites that is.
Check Your Fleet
Single site with wp-cli:
wp core version --path=/home/USERNAME/public_html
⚠️ On a cPanel/WHM box, find every install first, including the ones nobody documented. This reads version.php directly, so it's fast across hundreds of accounts and doesn't need wp-cli:
for d in /home/*/public_html; do [ -f "$d/wp-includes/version.php" ] && printf '%s: %s\n' "$d" "$(grep -oP "\\\$wp_version = '\K[^']+" "$d/wp-includes/version.php" 2>/dev/null)"; done
For installs in subdirectories or addon domains:
find /home/*/public_html -maxdepth 3 -name version.php -path '*/wp-includes/*' 2>/dev/null | while read f; do printf '%s: %s\n' "$f" "$(grep -oP "\\\$wp_version = '\K[^']+" "$f")"; done
Anything reporting 6.9.0-6.9.4 or 7.0.0-7.0.1 needs the RCE fix now. Anything on 6.8.0-6.8.5 needs 6.8.6 for the SQLi.
Patch, and Verify It Landed
WordPress is forcing auto-updates, so most sites will update themselves. ⚠️ WordPress has not said whether the forced push reaches sites that turned auto-updates off, and in practice they're off constantly: WP_AUTO_UPDATE_CORE set false, AUTOMATIC_UPDATER_DISABLED defined, a management plugin overriding it, or a read-only filesystem blocking it. Do not assume it landed. Verify.
Find accounts with auto-updates disabled:
grep -rlE "AUTOMATIC_UPDATER_DISABLED|WP_AUTO_UPDATE_CORE.*false" /home/*/public_html/wp-config.php 2>/dev/null
Update a site manually:
wp core update --path=/home/USERNAME/public_html
Then confirm the version actually moved. A silent permissions failure looks identical to success until you check.
The Gotcha If You Hardened wp-admin
⚠️ This one will catch people. If you protected /wp-admin with HTTP basic auth (a good practice I've written up for cPanel), it does nothing here. The REST API lives at /wp-json/, not under /wp-admin, and public endpoints are unauthenticated by design. Basic auth on the admin directory never sees a /wp-json/batch/v1 request.
Same for everything else scoped to wp-admin or wp-login.php: 2FA, IP allowlists, login rate limiting. None of it applies. If your mental model is "wp-admin is locked down so we're covered," wp2shell walks straight past it.
If You Can't Patch This Hour
Every stopgap comes down to keeping anonymous callers off the batch endpoint. All three break legitimate integrations, so they're bridges to a patch, not settled config.
⚠️ The critical WAF detail Searchlight is explicit about: you must block BOTH routes. The endpoint is reachable two ways, and a rule covering only the path leaves the query-string form open:
/wp-json/batch/v1rest_route=/batch/v1
Block only the first and you've done nothing. Nginx, covering both:
location ~ ^/wp-json/batch/ { deny all; return 403; }
if ($arg_rest_route ~* "^/batch/") { return 403; }
Apache in .htaccess, covering both:
RedirectMatch 403 ^/wp-json/batch/
RewriteCond %{QUERY_STRING} (^|&)rest_route=/batch/ [NC]
RewriteRule .* - [F]
⚠️ Test before rolling out. The block editor, some page builders, and various plugins use REST batch requests, so this can break the editor. It's a stopgap for the hours until your patch window, not a permanent rule. The other two options Searchlight lists are disabling the REST API wholesale (heavy-handed, breaks anything that depends on it) or a drop-in that rejects anonymous /batch/v1 at rest_pre_dispatch.
WAF Coverage
Cloudflare deployed two rules at 17:03 UTC July 17, covering all plans including free, as long as traffic is proxied:
| Rule | Managed Ruleset ID | Free Ruleset ID | Action |
|---|---|---|---|
| WordPress, SQLi in WP_Query | 1c060d3a371549219ee290d7ed933fcc |
db003b39b7774859a8d588ce33697a1a |
Block |
| WordPress, Unauthenticated RCE | 7dfb2bd4708d4b88b9911dc0550664b6 |
ebd3f2df15c74ddcbf6220c9b5ec246a |
Block |
Wordfence shipped a firewall rule to Premium, Care, and Response customers on July 17. ⚠️ Free Wordfence users don't get it until August 16, 2026, the standard 30-day delay. If you rely on free Wordfence and can't patch, you have no WAF coverage from it for a month, so the server-level batch block above matters more for you.
⚠️ Two things to verify rather than assume with any WAF. A ruleset-level override flipping everything from Block to Log (common during a false-positive hunt, then forgotten) means these new rules are logging exploitation attempts, not stopping them. And a WAF is a speed bump that variants bypass, not a patch. Both vendors say so themselves.
What To Watch For
Unauthenticated, no interaction, so opportunistic scanning finds sites before targeted attackers do. And with no CVE and no public signature yet, THN's point is worth heeding: nobody's really looking, which cuts both ways. Watch for:
- Requests to
/wp-json/batch/v1orrest_route=/batch/v1from sources with no reason to call it. - POSTs to REST endpoints from IPs with no prior session history.
- New PHP files under
wp-content/uploads/after July 17, the classic post-RCE webshell drop. - New admin users, or existing users' roles changing.
- Modified
.htaccessfiles, used for redirect injection and to hide shells.
Sweep for recently-modified PHP in upload directories, which should essentially never exist:
find /home/*/public_html/wp-content/uploads -name '*.php' -mtime -7 2>/dev/null
⚠️ Any hit there is a finding, not a quirk. uploads holds media, not executable PHP. Treat a hit as an active compromise and work it forensically: preserve first, find the entry point, hunt for persistence beyond the obvious shell. The forensic order is in why Linux servers get compromised, and the behavioral signals to hunt are in catching a compromise early. One webshell is rarely the only thing left behind.
The Disclosure Clock
The context worth understanding: Searchlight is holding wp2shell's technical details, but WordPress core is open source, and 7.0.1 and 7.0.2 both sit in the public release archive. Anyone can diff the two and find the bug. When Drupal patched an anonymous core SQL injection in May, Searchlight itself turned that public fix into a working teardown with two PoCs the same day. Nobody's obligated to do that to WordPress, but a day is what it took last time.
That's the bind for every open-source project: you can't ship the fix without shipping the map to the bug. The only lever is how fast the patch reaches sites before someone reads the diff. WordPress pulled it with forced updates on Friday. Whether the patch got there first is a race, and the version stats will tell the story after the fact.
Bottom Line
Unauthenticated RCE in WordPress core, no login, no interaction, on 6.9 through 7.0.1, chainable from an SQL injection reaching back to 6.8. There's no CVE yet, so your scanners are blind and you must track this by version number. Patch to 7.0.2, 6.9.5, or 6.8.6 for your branch and verify the version actually changed rather than trusting auto-update. If you host WordPress, inventory every install on the box tonight, subdirectories included. If you must delay, block both the /wp-json/batch/v1 path and the rest_route=/batch/v1 query form, not just one. And if you've hardened wp-admin and feel safe, you aren't: the REST API doesn't live there.
References
- Searchlight Cyber / Assetnote: wp2shell, Pre-Authentication RCE in WordPress Core (source)
- The Hacker News: New wp2shell WordPress Core Flaw Lets Unauthenticated Attackers Run Code (source)
- Wordfence: PSA on the WordPress Core Unauthenticated RCE Chain (source)
- Cloudflare: WAF Protects WordPress From Two High-Severity Vulnerabilities (source)
- WordPress 7.0.2 Security Release Announcement
- WordPress Version 7.0.2 Documentation
- wp2shell self-check tool