Quick answer
A WordPress WAF is either a plugin that inspects requests inside PHP on your own server, or a cloud firewall that filters traffic before it reaches the server. A plugin understands WordPress users and roles and is hard to route around, but every request it blocks still costs your server work. A cloud WAF stops floods and brute force before the origin, provided the origin is firewalled. Neither replaces updates: 91% of 2025's new WordPress vulnerabilities were in plugins.
01What is a WordPress WAF?
A WordPress WAF is a web application firewall tuned for WordPress: it inspects HTTP requests and blocks those that look like attacks on wp-login.php, xmlrpc.php, the REST API or known plugin flaws. The question that matters is not which product, but where it runs.
A plugin WAF runs on your server, inside PHP. Some load through WordPress itself; others use PHP's auto_prepend_file setting, which the PHP manual describes as a file automatically parsed before the main file, so they act before WordPress loads. Either way the request has already crossed your network link, been accepted by the web server and occupied a PHP worker.
A cloud WAF runs on the provider's network. DNS points at the provider, which terminates TLS, filters each request and forwards only what passes to your origin. A blocked request never reaches your server at all.
02Plugin WAF vs cloud WAF: side by side
Both are real firewalls, and they fail in different ways. The table sets out the trade-off.
| Plugin (endpoint) WAF | Cloud (DNS-level) WAF | |
|---|---|---|
| Runs | Inside PHP on your server | On the provider's network |
| Sees a request | After it reaches your server | Before it reaches your server |
| Cost of a blocked request | A PHP worker and CPU on your host | Nothing on your host |
| Knows the logged-in user and role | Yes | No; it sees cookies, not sessions |
| Can be bypassed | Not by changing the route; one loaded only by WordPress misses PHP files that run without it | Yes, if the origin is reachable directly |
| Volumetric DDoS | No | Yes, if the network has the capacity |
| Malware scanning, file integrity | Often included | No |
| Caching | No (a separate plugin) | Usually included |
| Setup | Install a plugin | Change DNS, then firewall the origin |
03What can each type of WordPress firewall stop?
A plugin cannot stop anything that overwhelms the server before PHP runs, and a cloud WAF cannot reliably stop an attack that looks like a normal, authorised request. Most sites need the cloud layer for volume and either a plugin or disciplined updating for the WordPress-specific rest.
| Threat | Plugin WAF | Cloud WAF | What actually fixes it |
|---|---|---|---|
| Volumetric flood | No; the link is full first | Yes | Upstream capacity |
| HTTP flood on uncached pages such as search or cart | Weak; each block costs PHP | Yes, with rate limits and challenges | Edge filtering and caching |
| Brute force on wp-login.php | Yes, but each attempt still runs PHP | Yes, before PHP | Rate limits, 2FA, unique passwords |
| XML-RPC system.multicall guessing | Yes | Yes, if XML-RPC is filtered | Disable or restrict xmlrpc.php |
| Exploit with a generic payload (SQL injection, XSS) | Usually | Usually | Updating the plugin |
| Broken access control in a plugin | Sometimes, with WordPress-aware rules | Rarely; the request looks legitimate | Updating the plugin |
| Stolen administrator password | Partly, with login protection | Partly, with login rules | Two-factor authentication |
| Attacker sending traffic to the origin IP | Still inspected | Bypassed completely | Firewall the origin to the WAF |
04How to protect wp-login.php and xmlrpc.php
These two files attract most automated abuse. WordPress's own security guide calls xmlrpc.php a frequent brute-force target, especially its system.multicall method, which Sucuri documented in 2015 being used to try hundreds of passwords inside a single HTTP request. XML-RPC's pingback feature was also abused in 2014 to turn more than 162,000 WordPress sites into an HTTP flood against one target.
If you control the web server, rate-limit login attempts there rather than in PHP, and close XML-RPC unless something you use needs it; some mobile apps and remote publishing tools still do. The configuration below passed nginx -t on nginx 1.22.1 and was then run in an isolated network namespace: GET requests to the login form were never limited, the twelfth rapid POST from one address received 429 Too Many Requests, xmlrpc.php returned 403, and a request by bare IP got no site at all.
- The map gives every non-POST request an empty key, and nginx does not count requests whose key is empty, so visitors loading the form are never limited
- rate=10r/m with burst=10 allows a short run of mistyped passwords before returning 429; tune it to your users, as explained in what rate limiting is
- Behind a cloud WAF the realip lines are essential: without them every request appears to come from the WAF, and one shared limit locks everyone out
- Trust the forwarding header only from the provider's published ranges, and use the header name the provider documents
- Closing XML-RPC also stops pingbacks, which make your server fetch attacker-chosen URLs and so reveal its address
# --- http {} context -------------------------------------------------
# 1. Behind a cloud WAF: restore the visitor's address from the proxy header.
# Use the ranges and header name your provider documents.
set_real_ip_from 192.0.2.0/24;
set_real_ip_from 2001:db8::/32;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# 2. Count only POSTs to the login form; GETs get an empty key (not limited).
map $request_method $wp_login_key {
POST $binary_remote_addr;
default "";
}
limit_req_zone $wp_login_key zone=wp_login:10m rate=10r/m;
limit_req_status 429;
# 3. Anything that reaches the origin by bare IP or an unknown name.
server {
listen 80 default_server;
listen [::]:80 default_server;
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
ssl_reject_handshake on;
return 444;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name example.com;
ssl_certificate certs/example.com.crt;
ssl_certificate_key certs/example.com.key;
root /var/www/example.com;
index index.php;
location = /wp-login.php {
limit_req zone=wp_login burst=10 nodelay;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
# 4. XML-RPC: closed unless a service you use genuinely needs it.
location = /xmlrpc.php {
# allow 203.0.113.0/24; # e.g. a publishing tool's documented range
deny all;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
}05How to stop attackers bypassing a cloud WAF
Moving DNS behind a cloud WAF only helps if the old route is closed. Attackers find origin addresses through DNS history, forgotten subdomains, mail headers and certificate transparency logs, then send traffic straight to the server. The fix is a firewall on the origin that accepts web traffic only from the WAF's published ranges; the full checklist is in how to hide your origin IP. This ruleset was syntax-checked and loaded with nft 1.0.6 in an isolated namespace.
- Fill the sets from the provider's published list and refresh them when it changes
- Drop UDP 443 as well if the origin serves HTTP/3, because QUIC is a separate path
- Keep the default nginx server from the previous section, so a request by bare IP gets neither a site nor a certificate
- If the origin address was ever public, change it once the firewall is in place
- Check from outside: forcing a request to the origin address, for example with curl --resolve example.com:443:ORIGIN_IP https://example.com/, should now time out
#!/usr/sbin/nft -f
# WordPress origin behind a cloud WAF: web ports only from the WAF's ranges.
table inet wp_origin
delete table inet wp_origin
table inet wp_origin {
set waf4 {
type ipv4_addr
flags interval
elements = { 192.0.2.0/24, 198.51.100.0/24 }
}
set waf6 {
type ipv6_addr
flags interval
elements = { 2001:db8::/32 }
}
chain input {
type filter hook input priority filter; policy accept;
iif "lo" accept
tcp dport { 80, 443 } ip saddr @waf4 accept
tcp dport { 80, 443 } ip6 saddr @waf6 accept
tcp dport { 80, 443 } counter drop
udp dport 443 counter drop
}
}06Why patching plugins matters more than any WAF
Almost all WordPress vulnerabilities are in plugins and themes, not in WordPress itself. Patchstack counted 11,334 new vulnerabilities in the WordPress ecosystem in 2025: 91% were in plugins, 9% in themes, and only six in core. 46% had no fix by the time they were disclosed, and for high-impact vulnerabilities, weighted by observed attack activity, the median time to first exploitation was five hours.
A WAF can block payloads it recognises and buy time with virtual patches. It cannot fix a plugin that hands administrator rights to the wrong user, because that request looks exactly like a legitimate one; updating or removing the plugin is the fix. The vulnerability write-ups on this site follow the same approach for server software: what is affected, how to check, and what an edge filter can and cannot do.
- Remove plugins and themes you do not use; deactivated code still sits on disk
- Turn on automatic updates for plugins you trust to ship safe minor releases
- Follow a vulnerability feed for the plugins you actually run
- Replace plugins that are abandoned or slow to fix disclosed issues
- Use two-factor authentication for every administrator, because a stolen password walks past every rule above; see credential stuffing
07Which WordPress firewall should you use?
For most sites the answer is both layers: the cloud WAF does the heavy lifting, and a plugin or simply prompt updating covers what only WordPress can see.
| Situation | Sensible setup |
|---|---|
| Small blog on shared hosting | Cloud WAF in front, prompt updates and 2FA; a plugin only if you want malware scanning |
| WooCommerce or membership site | Cloud WAF for floods and login abuse, plus WordPress-aware rules for logged-in users |
| Site being flooded right now | Cloud WAF first, origin firewall second; see emergency onboarding |
| Agency running many sites | Cloud WAF for each site, a central update policy, origin firewalls managed as code |
| No control over DNS | Plugin WAF and server-level rate limits, accepting that floods will still reach you |
Questions
Frequently asked questions
Sources
Sources and verification
nginx configuration syntax-checked with nginx 1.22.1 (nginx -t against a scratch config) and exercised with curl in an isolated network namespace (no PHP backend, so allowed requests returned 502): GETs to wp-login.php were never limited, the 12th rapid POST received 429, xmlrpc.php returned 403, X-Forwarded-For was honoured only from the trusted range, a bare-IP HTTP request got no response and a TLS handshake for an unknown name was rejected. nftables ruleset syntax-checked and loaded with nft 1.0.6. Debian 12, October 2026.
- 01Brute Force Attacks (Advanced Administration handbook) · WordPress.org
- 02Brute Force Amplification Attacks Against WordPress XMLRPC (October 2015) · Sucuri
- 03More Than 162,000 WordPress Sites Used for Distributed Denial of Service Attack (March 2014) · Sucuri
- 04State of WordPress Security in 2026 · Patchstack
- 05Module ngx_http_limit_req_module · nginx.org
- 06Module ngx_http_realip_module · nginx.org
- 07Module ngx_http_ssl_module (ssl_reject_handshake) · nginx.org
- 08Description of core php.ini directives (auto_prepend_file) · PHP manual
Last updated