wp2shell: one anonymous request and your WordPress is no longer yours

On Friday morning our to-do list shrank to a single question: which of our clients is still running WordPress on the 6.9 branch.
The reason: a July 17 advisory from Searchlight Cyber describes something you see in WordPress core once every few years. Code execution on the server, no login, no plugin, on a default install.
This is not a bug in some contact-form plugin. It is the core, present in every install.
What exactly was found
Adam Kues of the Assetnote team (Searchlight Cyber) reported a chain of two bugs.
The first is a SQL injection in the author__not_in parameter of the WP_Query class, tracked as CVE-2026-60137. TF1T, dtro, and haongo reported it independently. On its own it lets an attacker pull data from the database that they should never reach.
The second is in the bulk REST API endpoint at /wp-json/batch/v1. It is a route confusion, tracked as CVE-2026-63030 and classified as CWE-436, an interpretation conflict. CVSS 9.8.
Together they add up to unauthenticated code execution. Searchlight is deliberately holding back the technical details to give defenders time. What it did publish is a page where you can check your own instance and a ready-made plugin that rejects anonymous requests to the batch endpoint.
Cloudflare adds one important detail: the vulnerable path is reachable only when the instance runs without a persistent object cache. That covers most small and mid-size sites, because Redis or Memcached is rare at that level.
Who this affects
- Versions 6.9.0 to 6.9.4: vulnerable to both bugs. The fix is in 6.9.5.
- Versions 7.0.0 and 7.0.1: vulnerable to both bugs. The fix is in 7.0.2.
- The 6.8 branch: vulnerable only to the SQL injection. The fix is in 6.8.6.
- Anything older than 6.8: not affected.
- The 7.1 beta: fixed in 7.1 beta2.
WordPress took this seriously enough to trigger forced automatic updates for the vulnerable versions. That is the right call, and in a moment I will explain why it still does not close the matter.
What this means for your business
Code execution on a web server is more than a defaced home page. The attacker gets a PHP process, and with it the wp-config.php file that holds full database credentials. That database usually holds contact-form submissions, email addresses, sometimes CVs from the careers page, sometimes order data when WooCommerce is in play.
In practice that is a personal-data breach, 72 hours to notify the supervisory authority under GDPR, and a conversation with the board about why a marketing site was holding recruitment data.
Then there is shared hosting. It rarely ends with one compromised instance, because providers isolate neighbors in the same parent directory worse than the sales page promises.
Four things we see with every patch like this
The problem is rarely the update itself. It is what nobody counted.
Nobody knows how many WordPress sites they run. The main site is always in the inventory. But when we map an attack surface, we regularly turn up a campaign page from two years ago on its own subdomain, a test copy at an address the agency forgot to switch off, and a blog someone moved 'temporarily' to another host. These instances have no owner and no monitoring, and they are the ones still running 6.9.3 a quarter from now.
A forced update and a confirmed update are two different things. A WordPress core update can quietly fail to apply: file permissions after a migration, a container with a read-only filesystem, a WP_AUTO_UPDATE_CORE line set to false by a developer three years ago because something broke once. Nobody turned it back on. Open the admin panel and check the version number.
A partial block is no block at all. The endpoint answers in two forms: /wp-json/batch/v1 and ?rest_route=/batch/v1. The second works even where pretty permalinks are turned off. A rule blocks the first path, someone ticks the task off, and the second stays open. We saw exactly this pattern with earlier REST API patches, and I expect the same here.
The quiet will not last. As of July 18, nobody has published working code or confirmed exploitation in the wild. That means little. WordPress is open source, and the 7.0.1 and 7.0.2 packages are available side by side in a public archive. Diffing two versions is the standard route from a silent patch to a working exploit, and mass-attacking WordPress has been a steady business for years.
What to do today
- Update core to 7.0.2, 6.9.5, or 6.8.6, depending on your branch. Then open the admin panel and see which version is actually there.
- List every WordPress instance in the company, including the ones you do not maintain yourself. TLS certificates and DNS records are the fastest way to remember the subdomains you forgot.
- If an update is not possible today, block anonymous access to both forms of the endpoint or install the plugin that rejects such requests in
rest_pre_dispatch. That is a stopgap and can break working integrations. - Preserve your web server, WAF, and hosting logs before you do anything else. If it turns out someone got there first, they are the only evidence you will have.
- Review admin accounts, files modified since the day the vulnerable version went in, scheduled WP Cron jobs, and any plugins nobody knowingly installed.
Do not go hunting for ready-made indicators of compromise with file names or IP addresses. The public advisories do not give them, so anything circulating under that label is guesswork.
One takeaway for the longer term
For years the WordPress REST API was treated as an internal interface for the block editor. It is on by default, it answers anonymously, and it grows with every core release. Every time we ask on a project whether anyone has checked what you can call on /wp-json without logging in, the answer is 'it is only an API'.
How many WordPress instances does your company run? Ask that at your next team meeting and count how many times you hear 'probably' or 'we would have to check with the agency'. The number of those maybes is your real attack surface.
It starts with a short call with a consultant: 30 minutes, no obligation. We will inventory every WordPress instance you run, including the ones forgotten on old subdomains, and check which are actually exposed to the internet and can be taken over.