| Server IP : 104.21.73.115 / Your IP : 216.73.217.154 Web Server : Apache System : Linux vps42439 6.8.0-136-generic #136~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Fri Jul 3 16:29:11 UTC x86_64 User : dh_7hi3h9 ( 6349477) PHP Version : 8.3.30 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /etc/modsecurity/mod_sec3_CRS/ |
Upload File : |
# =============================================================================
# wp2shell virtual patch for ModSecurity (Apache mod_security2 / libmodsecurity3)
# -----------------------------------------------------------------------------
# Blocks the unauthenticated WordPress REST "batch/v1" route/handler-confusion
# SQL-injection -> RCE chain (wp2shell):
# CVE-2026-63030 REST API batch route/handler confusion (WP 6.9 - 7.0.1)
# CVE-2026-60137 WP_Query author__not_in SQLi (facilitator) (WP 6.8 - 7.0.1)
# Fixed in WordPress 7.0.2 / 6.9.5 / 6.8.6 (released 2026-07-17).
#
# THIS IS A STOPGAP, NOT A PATCH. It buys time for un-upgraded sites; it does not
# make an unpatched site safe. Push customers to update; retire these rules after.
#
# Design goal: block every currently-known reach vector AND bypass while causing
# no collateral damage to legitimate REST batch traffic. It does that by NOT
# blocking "a batch request" outright. It only blocks a batch request whose
# sub-request "path" is not single-slash-rooted (an absolute URL, protocol-
# relative //, leading backslash, or bare relative path). That non-rooted path is
# the "http://...:bad/" seed the exploit REQUIRES to trigger the index desync.
# Legitimate batch clients always send rooted paths (/wp/v2/...), so they pass.
#
# Covers: pretty-permalink (/wp-json/.../batch/v1), plain-permalink query
# (?rest_route=/batch/v1), plain-permalink POST body (rest_route=/batch/v1), the
# multipart "rest.route" name-normalization bypass (PHP folds . space [ -> _),
# case/trailing-slash/backslash/encoding variants, and (defense-in-depth) the
# nested-batch form used to reach the vulnerable dispatch depth.
#
# PREREQUISITES (verify on the fleet before relying on these rules):
# - SecRequestBodyAccess On (bodies must be parsed)
# - SecRuleEngine On (start in DetectionOnly for burn-in)
# - The body must be FULLY inspected. The reported Cloudflare bypass pads
# ~50-60 KB of junk ahead of the payload to slip under a body-inspection cap.
# ModSecurity's default SecRequestBodyLimit is 13107200 (~12.5 MB), so a
# 50-60 KB body is inspected in full by default -- but ONLY if the site is not
# configured with a low SecRequestBodyLimit + SecRequestBodyLimitAction
# ProcessPartial. Confirm:
# SecRequestBodyLimit 13107200
# SecRequestBodyLimitAction Reject # (not ProcessPartial)
# These are GLOBAL directives with fleet-wide impact -- verify current values,
# do not blindly override. See the deployment runbook.
# - Rule IDs 210005-210009 must not collide with existing custom rules. Renumber
# if they do (keep 210005 in phase:1 and the rest in phase:2).
#
# NOTE ON THE ARGS PATH SELECTORS: ModSecurity flattens JSON arrays into arg names
# like json.requests.array_0.path and multipart/form into requests[0][path]. The
# [^.]+ / [^\]]* wildcards make the selectors resilient to array-naming
# differences between ModSecurity v2 and v3/Coraza, but CONFIRM against your engine
# with the oracle test in the runbook before switching to blocking mode.
#
# NGINX LIMITATION: On Nginx with fastcgi_pass, the request body is consumed before
# ModSecurity phase 2, preventing body inspection. Rules 210006/210008 will NOT fire
# on Nginx. Rule 210020 (phase 1, at end of file) provides fallback protection by
# blocking unauthenticated batch endpoint access.
# =============================================================================
# ---- 210005 (phase:1) -------------------------------------------------------
# WordPress treats a broader set of media types as JSON than ModSecurity's stock
# JSON trigger does (see wp_is_json_media_type()). Force JSON body parsing for
# every media type WordPress would accept, so the phase:2 selectors below can see
# json.requests.*.path in the body. pass/nolog: this only sets the body processor.
SecRule REQUEST_HEADERS:Content-Type "@rx (?i)(?:^|[\s,])application/(?:[\w!#\$&-\^\.\+]+\+)?json(?:\+oembed)?(?:$|[\s;,])" \
"id:210005,phase:1,pass,nolog,t:none,\
ctl:requestBodyProcessor=JSON"
# ---- 210006 (phase:2) -------------------------------------------------------
# Pretty-permalink reach: /wp-json/.../batch/v1 (any depth, subdir, index.php
# PATHINFO, case, trailing / or \, raw or %-encoded). DENY only when a top-level
# batch sub-request "path" is NOT single-slash-rooted. If no such arg exists (a
# normal request, or a legit batch with rooted paths) the chain does not match.
# NOTE: Uses REQUEST_URI_RAW for Nginx - try_files rewrites REQUEST_URI to /index.php
SecRule REQUEST_URI_RAW "@rx (?i)/.*batch/v1[/\x5c]*(?:\?|$)" \
"id:210006,phase:2,deny,status:400,log,t:none,\
msg:'wp2shell: WordPress REST batch sub-request path not single-slash-rooted',\
logdata:'%{MATCHED_VAR_NAME}=%{MATCHED_VAR}',\
tag:'attack-injection-sql',tag:'wp2shell',tag:'CVE-2026-63030',tag:'CVE-2026-60137',chain"
SecRule ARGS:/^json\.requests\.[^.]+\.path$/|ARGS:/^requests\[[^\]]*\]\[path\]$/ "!@rx ^/(?![/\x5c])" "t:none"
# ---- 210007 (phase:2) -------------------------------------------------------
# Plain-permalink reach: rest_route=/batch/v1 in the query string OR the POST body
# (ARGS covers both). The name selector matches the raw aliases PHP normalizes to
# rest_route (underscore, dot, space/'+', unmatched '[', and leading spaces) --
# this is what closes the multipart "rest.route" bypass. Same rooted-path guard.
SecRule ARGS:/^[\x20]*rest[_\x2e\x20\x5b]route$/ "@rx (?i)^/batch/v1[/\x5c]*$" \
"id:210007,phase:2,deny,status:400,log,t:none,\
msg:'wp2shell: WordPress REST batch (rest_route) sub-request path not single-slash-rooted',\
logdata:'%{MATCHED_VAR_NAME}=%{MATCHED_VAR}',\
tag:'attack-injection-sql',tag:'wp2shell',tag:'CVE-2026-63030',tag:'CVE-2026-60137',chain"
SecRule ARGS:/^json\.requests\.[^.]+\.path$/|ARGS:/^requests\[[^\]]*\]\[path\]$/ "!@rx ^/(?![/\x5c])" "t:none"
# ---- 210008 (phase:2) -- defense-in-depth -----------------------------------
# Core does NOT allow /batch/v1 as a batch sub-route, but custom code can opt it
# into allow_batch v1. Where it does, require every NESTED routing path to be
# single-slash-rooted. Scoped tightly (only fires when a sub-request path is itself
# /batch/v1) so ordinary requests[] data in normal endpoint bodies is not treated
# as a route. Pretty-permalink (REQUEST_URI_RAW) form.
# NOTE: Uses REQUEST_URI_RAW for Nginx - try_files rewrites REQUEST_URI to /index.php
SecRule REQUEST_URI_RAW "@rx (?i)/.*batch/v1[/\x5c]*(?:\?|$)" \
"id:210008,phase:2,deny,status:400,log,t:none,\
msg:'wp2shell: nested WordPress REST batch path not single-slash-rooted',\
logdata:'%{MATCHED_VAR_NAME}=%{MATCHED_VAR}',\
tag:'attack-injection-sql',tag:'wp2shell',tag:'CVE-2026-63030',tag:'CVE-2026-60137',chain"
SecRule ARGS:/^json\.requests\.[^.]+\.path$/|ARGS:/^requests\[[^\]]*\]\[path\]$/ "@rx (?i)^/batch/v1[/\x5c]*$" "t:none,chain"
SecRule ARGS:/^json\.requests\.[^.]+(?:\.body\.requests\.[^.]+)+\.path$/|ARGS:/^requests\[[^\]]*\](?:\[body\]\[requests\]\[[^\]]*\])+\[path\]$/ "!@rx ^/(?![/\x5c])" "t:none"
# ---- 210009 (phase:2) -- defense-in-depth -----------------------------------
# Plain-permalink twin of 210008 (rest_route incl. normalization aliases).
SecRule ARGS:/^[\x20]*rest[_\x2e\x20\x5b]route$/ "@rx (?i)^/batch/v1[/\x5c]*$" \
"id:210009,phase:2,deny,status:400,log,t:none,\
msg:'wp2shell: nested WordPress REST batch (rest_route) path not single-slash-rooted',\
logdata:'%{MATCHED_VAR_NAME}=%{MATCHED_VAR}',\
tag:'attack-injection-sql',tag:'wp2shell',tag:'CVE-2026-63030',tag:'CVE-2026-60137',chain"
SecRule ARGS:/^json\.requests\.[^.]+\.path$/|ARGS:/^requests\[[^\]]*\]\[path\]$/ "@rx (?i)^/batch/v1[/\x5c]*$" "t:none,chain"
SecRule ARGS:/^json\.requests\.[^.]+(?:\.body\.requests\.[^.]+)+\.path$/|ARGS:/^requests\[[^\]]*\](?:\[body\]\[requests\]\[[^\]]*\])+\[path\]$/ "!@rx ^/(?![/\x5c])" "t:none"
# =============================================================================
# NGINX FALLBACK RULES (Phase 1 - before body consumed by fastcgi_pass)
# -----------------------------------------------------------------------------
# On Apache: These are redundant (body inspection rules 210006-210008 work)
# On Nginx: These provide protection where body rules fail (can't inspect body)
#
# Strategy: Block UNAUTHENTICATED access to batch endpoints
# - wp2shell exploit is unauthenticated
# - Legitimate batch API usage typically requires authentication
# - This provides strong protection without body inspection
# =============================================================================
# ---- 210020 (phase:1) -------------------------------------------------------
# Block unauthenticated POST to /wp-json/batch/v1 with JSON content-type
# Checks for WordPress auth cookie OR Authorization header
SecRule REQUEST_URI_RAW "@rx (?i)/.*batch/v1[/\x5c]*(?:\?|$)" \
"id:210020,phase:1,deny,status:403,log,t:none,\
msg:'wp2shell-nginx: Unauthenticated POST to batch endpoint blocked',\
logdata:'URI=%{REQUEST_URI_RAW}',\
tag:'attack-injection-sql',tag:'wp2shell',tag:'CVE-2026-63030',tag:'CVE-2026-60137',chain"
SecRule REQUEST_METHOD "@streq POST" "chain"
SecRule REQUEST_HEADERS:Content-Type "@rx (?i)(?:^|[\s,])application/(?:[\w!#\$&-\^\.\+]+\+)?json" "chain"
SecRule &REQUEST_HEADERS:Authorization "@eq 0" "chain"
SecRule &REQUEST_COOKIES:/^wordpress_logged_in_/ "@eq 0"