Dashboard
PHP:
8.2.30
OS:
Linux
User:
dh_nv8b2m
/
/
etc
/
modsecurity
/
mod_sec3_CRS
📤 Upload
📝 New File
📁 New Folder
Close
Editing: 40_wp2shell_virtual_patch.conf
# ============================================================================= # 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"
Save
Cancel