wordpress-hardening-plugin
An OWASP CRS 4.0+ plugin that adds 40+ extra rules to harden WordPress at the WAF — before PHP ever loads. It does not duplicate what CRS already does (libinjection @detectSQLi/@detectXSS on all arguments); it adds the semantic, typed-parameter validation CRS lacks — exactly where the 2025–2026 wave of AI-discovered WordPress plugin SQLi/XSS CVEs slips through.
→ GitHub · Full guide & deep-dive (every rule explained) · All CRS plugins
What it blocks
Every protection is individually configurable via SecAction toggles in wordpress-hardening-config.conf; sensible defaults are on out of the box. Highlights:
- Typed query-var validation — WP-core numeric vars (
p,page_id,attachment_id,m,year,paged,cpage…) must be integers;order=must beasc/descandorderby=a bare column name. The ORDER BY allowlist is strictly stronger than libinjection, which has documented ORDER BY bypasses — the #1 WordPress plugin SQLi class. - Endpoint lockdown — xmlrpc.php, user enumeration, literal
adminlogins, wp-cron.php, wp-json (exact match), the wp-admin theme/plugin editor, backup/archive/SQL-dump paths. - Info-leak & probe blocks — readme.html, license.txt, .user.ini, install.php, wlwmanifest.xml, debug.log; VCS/dotfile probes (.env, .git/, .svn/, .htpasswd); wp-config backup variants; PHP stream wrappers in args (php://, data://, phar://…).
- Known-CVE signatures — SureTriggers/OttoKit (CVE-2025-3102, CVE-2025-27007), Bricks Builder (CVE-2024-25600), CVE-2018-6389 load-scripts DoS, plus legacy scanner probes (revslider, timthumb, MailPoet, wp-file-manager, Duplicator).
- Login rate limiting — default 5 POST attempts per 60s per resolved client IP, replies HTTP 429 (RFC 6585). See the engine note below.
- GeoIP & IP-reputation — optional country gate on wp-login.php and an automatic IP/CIDR blocklist (both off by default).
- BREACH/CRIME tagging — flags compressible secret-bearing requests; real stripping is configured at the proxy (see the BREACH guide).
Client-IP handling
The blocked endpoints, the rate-limit counter, the GeoIP gate and the IP-reputation blocklist all share one client-IP resolver and one private-IP decision, so the same identity is used everywhere. Loopback and RFC 1918 / IPv6 ULA ranges are whitelisted by default, so internal cron, monitoring and load balancers reach these endpoints while external attacks are blocked. On a directly-exposed server, enable trusted-proxy pinning so a spoofed X-Forwarded-For can’t bypass the whitelist or rotate the rate-limit key.
Engine note: rate limiting
Rate limiting relies on persistent collections (initcol + IP: variables). This works reliably on Apache + mod_security2. libmodsecurity3 (the engine used by nginx / Angie) has long-standing gaps in persistent collections — the counter often never persists, so the limit never triggers even though the rule loads fine. On libmodsec3, prefer your web server’s native limiter (nginx/Angie limit_req zone=…) for /wp-login.php and treat this plugin’s rate-limiter as Apache-only.
Pair with the exclusions plugin
This plugin only adds blocks. Install the companion wordpress-rule-exclusions-plugin too — it suppresses known CRS false positives for WordPress so you can run in blocking mode without noise.
Install
Copy the plugin files into your CRS plugins/ directory; CRS auto-loads *-config.conf, then *-before.conf, then *-after.conf. Full install, configuration and per-rule reference is in the deep-dive guide. Need the engine first? See How to install ModSecurity + OWASP CRS on NGINX.