A type-check the developers thought was defense-in-depth turned out to be the only guard between the REST API and raw SQL
The bug lives in WP_Query: when author__not_in is passed as a string instead of an array, the is_array() sanitisation branch is skipped, (array)"payload" wraps it as ["payload"], and implode(',', ...) drops the attacker's raw string straight into a NOT IN (...) clause. Affected branches: 6.8.x < 6.8.6, 6.9.x < 6.9.5, and 7.0.x < 7.0.2. In isolation the REST API's author_exclude schema (type: array, items: integer) is supposed to block string values before they ever reach the vulnerable path — which is why WordPress scored it MEDIUM and set AC:H.
That MEDIUM label is wrong for anyone running WordPress 6.9 or 7.0. Adam Kues (Searchlight Cyber / Assetnote) disclosed this alongside CVE-2026-63030, a REST batch-route confusion bug that dispatches a categories request through the posts controller and bypasses the schema check entirely. Chained, an unauthenticated attacker gets SQLi → object-injection → pre-auth RCE on a stock WordPress install. A public PoC (Senanfurkan/wordpress-cve-2026-63030) landed the same day, and Cloudflare shipped emergency WAF signatures within 24 hours. Treat this CVE ID as the SQLi half of WP2Shell — not a standalone medium.
5 steps from start to impact.
Identify a WP 6.9 / 7.0 target with REST API reachable
/wp-json/, readme.html, generator meta, or /wp-includes/js/wp-embed.min.js hash. Mass scanning uses httpx + nuclei templates for WordPress version disclosure. Only 6.9.x < 6.9.5 and 7.0.x < 7.0.2 permit the unauthenticated RCE chain; 6.8.x is SQLi-only and requires a vulnerable plugin.- Target running WordPress 6.9.0–6.9.4 or 7.0.0–7.0.1
/wp-json/reachable from the attacker network (default on public installs)
- Sites behind Cloudflare with July-2026 WAF rules block the batch endpoint pattern
- Version fingerprint hidden by hardening plugins (Wordfence, iThemes Security)
Trigger batch route confusion (CVE-2026-63030)
/wp-json/batch/v1 with a payload that references the categories endpoint but is dispatched by the posts controller. This is the schema-mismatch primitive that lets an unsanitised author_exclude value survive into WP_Query. Weaponised in Senanfurkan/wordpress-cve-2026-63030 PoC, which posts a crafted JSON body via curl.- Anonymous access to
/wp-json/batch/v1 - WP core ≥ 6.9 (batch route did not exist in 6.8)
- Cloudflare, Sucuri, Wordfence all shipped rules blocking the specific batch-confusion pattern within 24h
- Sites that disable REST API for anonymous users (jetpack/rest-api-toolbox) are immune
Inject via author__not_in into WP_Query SQL
implode(',', ...) and is interpolated raw into the posts query. Attackers use a time-based or UNION payload — the sqlmap tamper script for this CVE surfaced the day after disclosure. Data exfil (users table, wp_users.user_pass) is trivial from this primitive.- Step 2 succeeded
- MySQL/MariaDB backend (SQLite driver is not vulnerable to the tested UNION payload)
DB_USERwith restricted grants (rare in shared hosting)- SQL query logging + anomaly detection on prod DB
NOT IN (SLEEP(... or UNION SELECT patterns against wp_posts; EDR unlikely to catchEscalate SQLi to RCE via PHP object injection
wp_options, and a follow-up request triggers unserialize() in an autoloaded option. This reaches known WordPress core POP gadgets, yielding code execution as the PHP-FPM/Apache user. This is what makes the WP2Shell label appropriate.- Attacker can write to
wp_options(implied by SQLi with INSERT/UPDATE grants — WordPress default) - Autoloaded option is fetched on a subsequent request
DISALLOW_FILE_MODSand read-only filesystems block webshell drop but not code exec- Some managed hosts (WP Engine, Kinsta) run PHP under a jailed user with no shell
php-fpm spawning sh/bash; File-integrity monitoring catches webshell writes to wp-contentPivot from the WordPress host
wp-config.php (DB creds), wp-content/uploads (webshell staging), and outbound egress. Mass campaigns will drop coin miners, SEO-poisoning shells, or use the box as a phishing infra pivot. Enterprise-hosted WordPress often shares AD-integrated auth or lives inside the corporate DMZ.- Egress allowed from the web server (default)
- No egress filtering or micro-segmentation
- Segmented DMZ with no lateral routes limits blast radius
- Immutable-infra WordPress (containers rebuilt on deploy) survives worse than long-lived VMs
The supporting signals.
| In-the-wild | Active mass scanning observed for /wp-json/batch/v1 within 48h of disclosure per GreyNoise tag WordPress WP2Shell RCE Attempt. No confirmed KEV listing as of 2026-07-18, but expected imminently. |
|---|---|
| Public PoC | Senanfurkan/wordpress-cve-2026-63030 — end-to-end pre-auth RCE chain published 2026-07-17. Nuclei + sqlmap tamper scripts followed. |
| EPSS | Score not yet published (disclosed 2026-07-17). Similar WordPress core pre-auth chains historically hit >0.90 within 7 days. |
| KEV status | Not listed as of 2026-07-18. Given PoC + mass-scan activity, add-date within 7 days is likely. |
| CVSS vector | Vendor: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N (5.9). noisgate reassess: AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H when chained → 9.6. |
| Affected versions | WordPress 6.8.0–6.8.5, 6.9.0–6.9.4, 7.0.0–7.0.1. 6.7.x and earlier not affected (author__not_in path predates the missing guard). |
| Fixed versions | 6.8.6, 6.9.5, 7.0.2. Debian/Ubuntu backports pending as of 2026-07-18; auto-updates rolling to sites with background updates enabled. |
| Exposure data | Shodan facet http.component:WordPress returns ~14.2M hosts; Censys estimates ~35–45% are on 6.9.x or 7.0.x branches — roughly 5–6M internet-exposed vulnerable instances. |
| Disclosure | Coordinated disclosure 2026-07-17 alongside WP 7.0.2 release. Cloudflare shipped emergency WAF rules same day. |
| Researcher | Adam Kues, Searchlight Cyber / Assetnote research team. |
noisgate verdict.
Chained with the same-day-disclosed CVE-2026-63030 batch-route bug, this SQLi becomes an unauthenticated pre-auth RCE on WordPress 6.9+ — the single most decisive factor is a public end-to-end PoC hitting a component with ~14M internet-exposed installs. Vendor's MEDIUM label reflects only the standalone SQLi in isolation, which is not how this CVE will be exploited in practice.
Why this verdict
- Chain flips the CVSS: standalone
AC:H/C:H/I:N/A:Nis fair, but chained with CVE-2026-63030 the attack complexity collapses toAC:Land impact expands toC:H/I:H/A:H. A CVE ID is not a threat model. - Role multiplier — public-facing web tier: WordPress is canonically the internet-facing marketing/CMS layer for a majority of enterprises. Chain succeeds → PHP-user shell → webshell → DMZ pivot. Blast radius: host → tenant → potential AD pivot where WP shares SSO. Deployment share in this role: >90% of installs are public-facing by definition — floor is CRITICAL.
- Weaponisation velocity: Public PoC same-day, WAF signatures same-day, GreyNoise tag within 48h. Mass exploitation window has already opened.
- Population is enormous: 5–6M vulnerable internet-exposed instances per Shodan/Censys facets. Even at 0.1% conversion, that is a five-figure victim count.
Why not higher?
There is no higher bucket. Score capped at 9.4 (not 10.0) because a small fraction of managed WordPress hosts (WP Engine, Kinsta, Automattic-hosted) auto-patched within hours of the 7.0.2 release, and Cloudflare WAF customers with default managed-rules had virtual patching within 24h — so real-world victim count will be materially lower than the raw exposed-population number.
Why not lower?
Downgrading below CRITICAL requires either no public exploit (there is one), narrow install base (14M+ exposed hosts), or high attacker prerequisites (chain is unauthenticated). None of the friction factors — WAF, hardening plugins, jailed PHP — apply to enough of the population to break the CRITICAL floor. Vendor MEDIUM is only defensible if you ignore the sibling CVE, which no attacker will.
What to do — in priority order.
- Block
/wp-json/batch/v1at the WAF or reverse proxy — The batch endpoint is the vector for the RCE chain and is rarely used by legitimate frontend traffic. Add a deny rule for POST to that path (or return 403 for anonymous requests) at Cloudflare/Akamai/F5/nginx. Deploy within 3 days per noisgate mitigation SLA — hours if you are internet-facing. - Force WordPress auto-updates to minor releases — Set
WP_AUTO_UPDATE_CORE = 'minor'(ortrue) inwp-config.phpon every managed WP host. 6.8.6 / 6.9.5 / 7.0.2 will flow automatically. Combine with a scheduledwp cron event run --due-nowif cron is disabled. Roll out within 3 days. - Enable Cloudflare / Sucuri / Wordfence emergency rules — All three vendors shipped CVE-2026-63030 / 60137 signatures on 2026-07-17. If you already pay for them, verify the ruleset version is post-disclosure and set to
block, notlog. Confirm rule ID coverage within 24 hours. - Restrict database grants on
wp_usersandwp_options— Even a successful SQLi is materially less useful if theDB_USERinwp-config.phpcannot write towp_options(blocks the unserialize gadget) or SELECT fromwp_users(blocks credential dump). Rotate creds and constrain grants within 30 days as a systemic hardening step.
- Disabling XML-RPC — different endpoint, does not touch the REST batch route or WP_Query path.
- Two-factor authentication on wp-admin — the chain is pre-auth against the REST API; no login is involved.
- IP-allowlisting
/wp-admin— same reason; the exploit hits/wp-json/, which is often left open for headless-CMS integrations. - Hiding the WordPress version — attackers spray the payload blind; version fingerprinting is a nice-to-have, not a control.
Crowdsourced verification payload.
Run this from an auditor workstation with network reach to the target site. Usage: ./check-cve-2026-60137.sh https://target.example.com. No credentials required — it inspects the WordPress REST API version banner and the batch endpoint. Non-zero exit indicates VULNERABLE.
#!/usr/bin/env bash
# noisgate: CVE-2026-60137 / CVE-2026-63030 (WP2Shell) detector
# Usage: ./check-cve-2026-60137.sh https://target.example.com
set -u
TARGET="${1:-}"
if [[ -z "$TARGET" ]]; then
echo "Usage: $0 <https://site>" >&2
exit 2
fi
# Strip trailing slash
TARGET="${TARGET%/}"
# 1. Fetch WordPress version from /wp-json/ (public endpoint)
BODY="$(curl -sS --max-time 10 -A 'noisgate-verifier/1.0' "$TARGET/wp-json/" || true)"
VER="$(echo "$BODY" | grep -oE '"gmt_offset"|"home"' >/dev/null && \
curl -sS --max-time 10 -A 'noisgate-verifier/1.0' "$TARGET/?rest_route=/" | \
grep -oE 'wp-json/wp/v2[^"]*' | head -n1)"
HDR_VER="$(curl -sSI --max-time 10 "$TARGET/wp-json/" | grep -i '^x-wp-' || true)"
RAW_VER="$(curl -sS --max-time 10 "$TARGET/readme.html" | grep -oE 'Version [0-9]+\.[0-9]+(\.[0-9]+)?' | head -n1 | awk '{print $2}')"
if [[ -z "$RAW_VER" ]]; then
echo "UNKNOWN: could not determine WordPress version at $TARGET"
exit 3
fi
echo "Detected WordPress version: $RAW_VER"
# 2. Compare against fixed versions per branch
vuln=1 # 1 = patched, 0 = vulnerable
IFS='.' read -r MAJ MIN PATCH <<<"$RAW_VER"
PATCH="${PATCH:-0}"
case "${MAJ}.${MIN}" in
6.8) [[ "$PATCH" -lt 6 ]] && vuln=0 ;;
6.9) [[ "$PATCH" -lt 5 ]] && vuln=0 ;;
7.0) [[ "$PATCH" -lt 2 ]] && vuln=0 ;;
*) # older branches: SQLi path predates guard; RCE chain N/A
[[ "$MAJ" -eq 6 && "$MIN" -lt 8 ]] && { echo "PATCHED: $RAW_VER predates vulnerable code path"; exit 0; } ;;
esac
# 3. Probe batch endpoint reachability (RCE chain requires it)
BATCH_CODE="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 \
-X POST -H 'Content-Type: application/json' \
--data '{"validation":"require-all-validate","requests":[]}' \
"$TARGET/wp-json/batch/v1")"
echo "Batch endpoint returned: HTTP $BATCH_CODE"
if [[ "$vuln" -eq 0 ]]; then
if [[ "$BATCH_CODE" =~ ^(200|400|401|403)$ && "${MAJ}.${MIN}" != "6.8" ]]; then
echo "VULNERABLE: $RAW_VER, batch endpoint reachable — pre-auth RCE chain viable"
exit 1
else
echo "VULNERABLE: $RAW_VER SQLi path present (batch route not confirmed reachable)"
exit 1
fi
fi
echo "PATCHED: $RAW_VER is at or above fixed release"
exit 0
If you remember one thing.
POST /wp-json/batch/v1 and confirm your Cloudflare/Sucuri/Wordfence emergency ruleset is loaded; (3) run the verification script across your inventory and treat any VULNERABLE result on an externally reachable host as an incident-response candidate — assume attempted exploitation. noisgate mitigation SLA: ≤ 3 days (WAF block + emergency ruleset). noisgate remediation SLA: ≤ 90 days for full 7.0.2 rollout, but for public-facing hosts you should be done inside a week — mass exploitation is already underway per GreyNoise telemetry. If KEV listing lands mid-week, federal defenders get a hard 21-day BOD 22-01 clock; the rest of us should already be finished.Sources
- Cloudflare Blog — WordPress WAF protection for CVE-2026-60137 / 63030
- Beazley Security Labs — WP2Shell advisory (BSL-A1193)
- SecurityOnline — WordPress Pre-Auth RCE CVE-2026-63030 PoC public
- GitHub PoC — Senanfurkan/wordpress-cve-2026-63030
- Cyber Kendra — WP2Shell critical RCE writeup
- THREATINT — CVE-2026-60137 record
- Feedly CVE — CVE-2026-63030 sibling record
- WordPress security release announcement (7.0.2 / 6.9.5 / 6.8.6)
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.