From b89243807047de1e7d2a11ef51d81d607ba61fbb Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Mon, 20 Jul 2026 07:02:45 -0700 Subject: [PATCH] feat(waf): block anonymous WP REST batch endpoint (wp2shell CVE-2026-63030) Adds a frontend virtual patch denying /wp-json/batch/v1 and the ?rest_route=/batch/v1 fallback (including the %2F-encoded spelling) for requests without a wordpress_logged_in_* cookie. wp2shell chains CVE-2026-60137 (core SQL injection) with CVE-2026-63030 (REST batch-route confusion) into unauthenticated RCE on WP 6.9.0-6.9.4 and 7.0.0-7.0.1. Exploits are public and were used against this fleet on 2026-07-19/20; one site was compromised through this path, including a re-injection of wp-includes/plugin.php nine minutes after it was patched. Anonymous-only by design: batch/v1 is used legitimately by the block editor for multi-entity saves, so a blanket deny would break wp-admin. Placed ahead of the SPOE handoff so blocked requests never cost a WAF round-trip, and it works regardless of Coraza mode (whp02 and sdbees run detect_only, where a Coraza rule would log but not block). This is a virtual patch, not a fix. It removes reachability only, and stays until every site is confirmed on a fixed release. Co-Authored-By: Claude Opus 4.8 (1M context) --- templates/hap_listener.tpl | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/templates/hap_listener.tpl b/templates/hap_listener.tpl index 6ff9709..bf0e6fd 100644 --- a/templates/hap_listener.tpl +++ b/templates/hap_listener.tpl @@ -96,6 +96,35 @@ frontend web acl has_login_cookie req.cook(whplc) -m found http-request deny deny_status 403 if METH_POST wp_login_path !has_login_cookie !is_local !is_trusted_ip !is_whitelisted + # WordPress REST batch endpoint lockdown ("wp2shell": CVE-2026-63030 + + # CVE-2026-60137). Chaining a core SQL injection with REST batch-route + # confusion gives unauthenticated RCE on WP 6.9.0-6.9.4 and 7.0.0-7.0.1 + # (fixed in 6.9.5 / 7.0.2). Exploits are public and were used against this + # fleet on 2026-07-19/20; one site was compromised via this path before + # patching. This is a virtual patch: it does not repair the vulnerable + # application logic, it only removes reachability, so it stays until every + # site is confirmed on a fixed release. + # + # Both routing forms must be covered -- a rule matching only the pretty + # permalink path leaves the ?rest_route= fallback wide open, and urlp() + # does not URL-decode, hence the third ACL for the %2F spelling. + # + # Anonymous-only. batch/v1 is used legitimately by the block editor for + # multi-entity saves, so a blanket deny would break wp-admin for real + # users; requiring a wordpress_logged_in_* cookie costs them nothing. + # req.cook() needs an exact name and WordPress suffixes a per-site hash, + # so this substring-matches the raw Cookie header instead. + # + # Immediate deny, not tarpit -- holding connections open helps an attacker + # who is already scripting this. Honors the same whitelist as above. + acl wp_batch_path path_beg /wp-json/batch/v1 + acl wp_batch_route urlp(rest_route) -i -m beg /batch/v1 + acl wp_batch_route_enc query -i -m sub rest_route=%2Fbatch%2Fv1 + acl has_wp_logged_in req.hdr(Cookie) -i -m sub wordpress_logged_in_ + http-request deny deny_status 403 if wp_batch_path !has_wp_logged_in !is_local !is_trusted_ip !is_whitelisted + http-request deny deny_status 403 if wp_batch_route !has_wp_logged_in !is_local !is_trusted_ip !is_whitelisted + http-request deny deny_status 403 if wp_batch_route_enc !has_wp_logged_in !is_local !is_trusted_ip !is_whitelisted + # IP blocking using map file (manual blocks only) # Map file format: /etc/haproxy/blocked_ips.map contains " 1" per line # Runtime updates: echo "add map #0 IP_ADDRESS 1" | socat stdio /var/run/haproxy.sock