If you needed a reminder that severity ratings don't tell the whole story, July 2026 delivered one. On July 17, the WordPress ecosystem, which is a content management system running roughly four in every ten websites on the internet, was hit with a vulnerability nicknamed wp2shell. On its own, neither of the two bugs behind it looked catastrophic. Chained together, they gave an anonymous attacker, with no account, no plugin dependency, and no user interaction, a working shell on a default WordPress install.
wp2shell is the product of two separate WordPress Core flaws meeting at exactly the wrong intersection.
The first, tracked as CVE-2026-60137, is a SQL injection sitting inside "WP_Query", the class that powers nearly every database read WordPress performs. It expects a parameter called "author__not_in" to arrive as an array of numeric IDs. Arrays get sanitized automatically, and the code assumes that protection always applies. Feed it a string instead, and that assumption breaks; the raw value gets concatenated straight into the SQL WHERE clause. This bug has existed since WordPress 6.8, but by itself it stayed low-risk, because every route that exposed "author__not_in" required a login first.
The second, CVE-2026-63030, is what removed that login requirement. Since 2021, WordPress has shipped a REST API batch endpoint (/wp-json/batch/v1) that bundles multiple sub-requests into a single call. Internally, WordPress tracks those sub-requests in two parallel lists, one for permission checks, one for execution. A bug introduced in version 6.9 let those two lists fall out of sync whenever one sub-request failed to parse cleanly. The practical effect: request number six could execute using the permission context meant for request number five. By carefully nesting a malformed batch call, an attacker could get a protected, authenticated-only endpoint to run under the identity of a public, unauthenticated one.
Individually, this is a bounded SQL injection and a REST API logic bug. Combined, an anonymous attacker reaches the injection point without ever logging in, and from there, forged database rows are used to trigger legitimate WordPress features (cached embeds, customizer change-sets) in a sequence that ends with a rogue administrator account and an uploaded web shell.
The full remote-code-execution chain hits WordPress 6.9.0 - 6.9.4 and 7.0.0 - 7.0.1. The SQL injection alone reaches back to 6.8.0. Fixes were shipped in 6.9.5, 7.0.2, 6.8.6, and 7.1 Beta 2 when WordPress forced the update through its automatic-update system for eligible sites. However, any installation with auto-updates disabled, frozen on an old version for compatibility reasons, or simply forgotten (a staging subdomain, an old marketing micrsiteo) is still exposed.
By July 21, four days after disclosure, both CVEs had been added to CISA's Known Exploited Vulnerabilities catalog, confirming active exploitation in the wild. A public proof-of-concept had already been circulating on GitHub for three days by that point.
wp2shell isn't a one-off engineering failure; it is a pattern security teams should expect to see again. Modern web platforms are built from dozens of interacting subsystems, REST routing, query builders, caching layers, permission middleware, each individually reviewed and justified as individually "safe enough." The dangerous vulnerabilities increasingly live in the seams between them, not inside any single component. A permission check that's airtight in isolation can still be walked around if something upstream desynchronises the data it relies on.
That's also why version-number scanning and annual pentests keep missing these issues. A scanner that reads wp-version and checks it against a CVE list never actually tests whether the batch endpoint behaves correctly under a malformed nested request. A pentest report dated March says nothing about a flaw disclosed in July. And in almost every real-world case, the asset that gets compromised first is the one nobody remembered was still running, the old blog subdomain, the staging environment someone forgot to take offline.
For any organization running WordPress, the priority order is:
1. Patch immediately, 6.9.5, 7.0.2, or 6.8.6, depending on your branch. This is the only complete fix.
2. Block both forms of the batch route at the WAF or web server level: /wp-json/batch/v1 and the query-string variant ?rest_route=/batch/v1. Filtering only the first leaves the second wide open, and it's the one most WAF rulesets miss.
3. Restrict anonymous REST API access where the batch endpoint isn't actually needed by anonymous users, via a plugin or a rest_pre_dispatch hook.
4. Check for indicators of compromise even after patching, new administrator accounts, unfamiliar plugins, and unusual rows in wp_posts or wp_options are the tells. Active exploitation was confirmed within days, so "we patched late but nothing happened" is not a safe assumption without verification.
The broader lesson for any team maintaining a public-facing web estate, WordPress or otherwise, is that a CVSS score describes one vulnerability in isolation. It doesn't describe what happens when that vulnerability meets another one nobody thought to chain it with. Continuous, behaviour-based monitoring of exposed endpoints, not periodic snapshots, is what catches the combination before mass scanners do.
This article is part of our monthly cybersecurity roundup, covering the trends, incidents, and lessons shaping the threat landscape.