Dashboard
PHP:
8.4.20
OS:
Linux
User:
sinnett_country
/
/
etc
/
modsecurity
/
mod_sec3_CRS
📤 Upload
📝 New File
📁 New Folder
Close
Editing: 50_xp_login_xss.conf
# ============================================================================= # ModSecurity virtual patch — wp-login.php failed-username XSS # # STRATEGY # The injection's necessary-and-sufficient condition is a literal '<' (an HTML # tag opener) in the LOGIN USERNAME after URL-decoding. KSES cannot build a node # without one, and every evasion the harness exercises — whitespace class after # '<' (space/tab/LF/CR/FF/VT), case, alternate elements (area/a/img/div/button), # token-free structural payloads, form/multipart/decoy delivery — all reduce to # "a '<' in the decoded username". # # A real username never contains '<' (registration runs sanitize_user() in strict # mode, which strips it), so blocking it is false-positive-free. The rule targets # ONLY ARGS:log / ARGS:user_login — NOT the password, which is never reflected and # legitimately contains '<' (see benign_pwd_with_angle in the harness). # # PREREQUISITES (without these, an attacker can starve the rule of input): # SecRequestBodyAccess On # SecRequestBodyLimitAction Reject # do not ProcessPartial — see rule ...110 # # Reject multipart-parser desync so 'log' can't be smuggled past ModSecurity. # # (CRS ships this as 200003/200004; if you are not on CRS, keep the rule below.) # # Renumber the ids (15001xx) into a range your deployment leaves free. # Self-contained 'deny' rules; a CRS-anomaly-mode variant is noted at the bottom. # ============================================================================= # --- 0. Multipart integrity (skip if CRS 200003/200004 already active) ------- SecRule MULTIPART_STRICT_ERROR "!@eq 0" \ "id:210021,phase:2,deny,status:403,t:none,log,\ msg:'Malformed multipart body (possible parser-desync smuggling)',\ tag:'wp-login-xss',severity:'CRITICAL'" # --- 1. PRIMARY: HTML tag opener in the wp-login username -------------------- # Deny a tag opener '<' in the login username. The selector is a REGEX name # match (ARGS:/log/), NOT ARGS:log, and that difference is load-bearing: PHP # normalises POST parameter NAMES before populating $_POST, so several names # fold to 'log' while an exact ARGS:log selector — which matches the raw decoded # name — never fires. Confirmed bypasses of the exact selector (all inject on # stock WP, all missed by ARGS:log): # "%20log" / "+log" PHP strips a leading space -> $_POST['log'] # "log%00" / "log%00zz" PHP truncates the name at NUL -> $_POST['log'] # Every name PHP can fold to 'log'/'user_login' keeps "log" as a contiguous # substring (PHP only trims/truncates/underscores — it never inserts), so # ARGS:/log/ catches the whole class. It still never inspects pwd/user_pass # (no "log"), so a '<' in a password is not a false positive. # t:removeNulls + t:urlDecodeUni normalise the value; the multi-value ARGS # collection defeats parameter pollution (log=clean&log=<x). SecRule REQUEST_FILENAME "@endsWith /wp-login.php" \ "id:210022,phase:2,deny,status:403,t:none,log,\ msg:'WP login username HTML injection',\ logdata:'%{MATCHED_VAR_NAME} contained a tag opener',\ tag:'wp-login-xss',severity:'CRITICAL',chain" SecRule "ARGS:/log/" "@rx <" \ "t:none,t:urlDecodeUni,t:removeNulls" # --- 2. HARDENING: reject oversize login POSTs ------------------------------ # Kills the inspection-window evasion (oversize_prefix_pad): a payload placed # after ~200KB of filler that a ProcessPartial body limit would skip. A real # login body is a few hundred bytes; 16KB is very generous. Phase 1 rejects on # the header before the body is buffered. Pair with SecRequestBodyLimitAction # Reject for chunked bodies that carry no Content-Length. SecRule REQUEST_FILENAME "@endsWith /wp-login.php" \ "id:210023,phase:1,deny,status:413,t:none,log,\ msg:'Oversize wp-login.php POST (inspection-window evasion)',\ tag:'wp-login-xss',severity:'WARNING',chain" SecRule REQUEST_HEADERS:Content-Length "@gt 16384" "t:none" # --- 3. OPTIONAL, DISABLED BY DEFAULT: block the downstream REST JSONP sink -- # Rule 1500100 already stops the attack at its entry point — with no login # injection there is no planted ajaxurl DOM, so nothing weaponises JSONP via # THIS bug. Blocking JSONP therefore adds NO coverage here; it only hardens # against unrelated injection vectors, and it breaks a legitimate WordPress # REST feature (every _jsonp callback returns 403). We do not recommend using # this unless absolutely necessary. # # If you must enable it, prefer narrowing to the chain's own shape — a _jsonp # callback together with a _method override — which spares ordinary JSONP: # # SecRule REQUEST_URI "@rx (?i)(?:/wp-json/|[?&]rest_route=)" \ # "id:1500120,phase:2,deny,status:403,t:none,t:urlDecodeUni,log,\ # msg:'REST JSONP method-override blocked',\ # tag:'wp-login-xss',severity:'WARNING',chain" # SecRule &ARGS:_jsonp "@ge 1" "chain" # SecRule &ARGS:_method "@ge 1" # --- CRS anomaly-scoring variant --------------------------------------------- # If you run CRS in anomaly mode, replace "deny,status:403" on the chain starters # with: "block,setvar:'tx.anomaly_score_pl1=+%{tx.critical_anomaly_score}'" # and keep the id/msg/tag/chain. The blocking decision then defers to CRS.
Save
Cancel