Guides

7 min readUpdated

WordPress WAF: plugin vs cloud firewall

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) WAFCloud (DNS-level) WAF
RunsInside PHP on your serverOn the provider's network
Sees a requestAfter it reaches your serverBefore it reaches your server
Cost of a blocked requestA PHP worker and CPU on your hostNothing on your host
Knows the logged-in user and roleYesNo; it sees cookies, not sessions
Can be bypassedNot by changing the route; one loaded only by WordPress misses PHP files that run without itYes, if the origin is reachable directly
Volumetric DDoSNoYes, if the network has the capacity
Malware scanning, file integrityOften includedNo
CachingNo (a separate plugin)Usually included
SetupInstall a pluginChange 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.

ThreatPlugin WAFCloud WAFWhat actually fixes it
Volumetric floodNo; the link is full firstYesUpstream capacity
HTTP flood on uncached pages such as search or cartWeak; each block costs PHPYes, with rate limits and challengesEdge filtering and caching
Brute force on wp-login.phpYes, but each attempt still runs PHPYes, before PHPRate limits, 2FA, unique passwords
XML-RPC system.multicall guessingYesYes, if XML-RPC is filteredDisable or restrict xmlrpc.php
Exploit with a generic payload (SQL injection, XSS)UsuallyUsuallyUpdating the plugin
Broken access control in a pluginSometimes, with WordPress-aware rulesRarely; the request looks legitimateUpdating the plugin
Stolen administrator passwordPartly, with login protectionPartly, with login rulesTwo-factor authentication
Attacker sending traffic to the origin IPStill inspectedBypassed completelyFirewall 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
nginx: real client IP behind a WAF, login rate limit, XML-RPC closed, bare-IP requests refused
# --- 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
nftables: web ports on the origin only from the WAF (example ranges)
#!/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.

SituationSensible setup
Small blog on shared hostingCloud WAF in front, prompt updates and 2FA; a plugin only if you want malware scanning
WooCommerce or membership siteCloud WAF for floods and login abuse, plus WordPress-aware rules for logged-in users
Site being flooded right nowCloud WAF first, origin firewall second; see emergency onboarding
Agency running many sitesCloud WAF for each site, a central update policy, origin firewalls managed as code
No control over DNSPlugin WAF and server-level rate limits, accepting that floods will still reach you

Questions

Frequently asked questions

Not necessarily for blocking, but often for visibility. A cloud WAF stops floods and much automated abuse before your server; a plugin adds malware scanning, file-change alerts and rules that know which user is logged in. If you skip the plugin, keep updates prompt and use two-factor authentication, because those cover most of what it would have caught.

No. A plugin runs inside PHP after the request has reached your server, so a flood that fills your link or exhausts the web server never gets far enough for the plugin to act, and every request it does block still costs a PHP worker. A DDoS has to be filtered upstream, before the traffic reaches your host.

If nothing you use depends on it, yes. XML-RPC is a common brute-force target because system.multicall can test many passwords in one request, and its pingback feature has been abused for floods. Some mobile apps and remote publishing tools still use it; in that case restrict it to the addresses that need it and rate-limit it.

For most visitors it does the opposite, because cached pages are served from the provider's network and the origin handles fewer requests. Logged-in users, carts and admin pages are not cached, so they see the extra hop without the caching benefit. A plugin WAF, by contrast, adds work to every request your own server handles.

It can if rules or caching are too aggressive. Make sure pages for logged-in users, carts and checkout bypass the cache, and that admin and REST API requests are not challenged in ways the editor cannot complete. Test the block editor, checkout and payment callbacks after switching, and allow the payment provider's webhook addresses if needed.

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.

  1. 01Brute Force Attacks (Advanced Administration handbook) · WordPress.org
  2. 02Brute Force Amplification Attacks Against WordPress XMLRPC (October 2015) · Sucuri
  3. 03More Than 162,000 WordPress Sites Used for Distributed Denial of Service Attack (March 2014) · Sucuri
  4. 04State of WordPress Security in 2026 · Patchstack
  5. 05Module ngx_http_limit_req_module · nginx.org
  6. 06Module ngx_http_realip_module · nginx.org
  7. 07Module ngx_http_ssl_module (ssl_reject_handshake) · nginx.org
  8. 08Description of core php.ini directives (auto_prepend_file) · PHP manual

Last updated