Every WordPress site is attacked by bots that try to guess passwords through two files: wp-login.php (the login page) and xmlrpc.php (an old remote-access interface). On a busy server, thousands of these requests a minute can use up the CPU and slow down every site, even when no password is guessed.
The best place to stop them is before they reach the server. If your sites use Cloudflare, a few free rules do that. If they do not, CSF on a cPanel server can block the attacking IP addresses instead.
Quick answer
With Cloudflare (free plan is enough), in Security › WAF › Custom rules, create two rules: Cloudflare
| Rule | Expression | Action |
|---|---|---|
| Block XML-RPC | (http.request.uri.path contains "/xmlrpc.php") |
Block |
| Protect login | (http.request.uri.path contains "/wp-login.php") |
Managed Challenge |
Without Cloudflare, on a cPanel server: let CSF read the Apache access logs and block IP addresses that request these files too often (see Option 2).
Before you block xmlrpc.php
Most sites do not use xmlrpc.php any more: the WordPress mobile app, the block editor and most plugins now use the REST API. A few still do, for example Jetpack and some older publishing tools. If a site uses one of them, do not block xmlrpc.php for it; protect it with rate limiting instead.
Option 1: Stop the attacks at Cloudflare
This works only for domains whose DNS records are Proxied (orange cloud) in Cloudflare.
Rule 1: Block xmlrpc.php
-
In the Cloudflare dashboard, choose the domain and go to Security › WAF › Custom rules.
-
Click Create rule and name it
Block XML-RPC. -
Click Edit expression and enter:
(http.request.uri.path contains "/xmlrpc.php") -
Under Choose action, select Block.
-
Click Deploy.
contains also catches WordPress sites installed in a subfolder, such as /blog/xmlrpc.php.
Rule 2: Challenge the login page
-
Create another custom rule named
Protect login. -
Expression:
(http.request.uri.path contains "/wp-login.php") -
Action: Managed Challenge. Real people pass it (usually without noticing); most bots do not.
-
Deploy.
Even stricter: if only a few people log in from fixed IP addresses (an office, for example), allow just those and block everyone else. Cloudflare's own example does this for /wp-admin/: Cloudflare
(not ip.src in {203.0.113.10 198.51.100.0/24} and http.request.uri.path contains "/wp-login.php")
with the action Block. Replace the addresses with your own.
Rule 3 (optional): Rate limit the login page
A rate limiting rule blocks an IP address that sends too many requests in a short time. In Security › WAF › Rate limiting rules: Cloudflare
- Create a rule named
Login rate limit. - Match requests where URI Path contains
/wp-login.php. - Set the rate, for example 5 requests per 10 seconds, counted per IP.
- Action: Block, then Deploy.
What the free plan includes: Cloudflare Cloudflare
| Feature | Free | Pro |
|---|---|---|
| Custom rules | 5 | 20 |
| Rate limiting rules | 1 | 2 |
| Rate limit counting period | 10 seconds | up to 1 minute |
Check it works
Open https://example.com/xmlrpc.php in a browser: you should see a Cloudflare block page. Open https://example.com/wp-login.php in a private window: you may briefly see a Cloudflare check before the login form. The Security › Events page shows what each rule has blocked or challenged.
Option 2: Block attackers on a cPanel server with CSF
If a site is not behind Cloudflare, CSF's login failure daemon (lfd) can watch the Apache logs and block IP addresses that request wp-login.php or xmlrpc.php too often. CSF must already be installed; see How to Install CSF Firewall.
Step 1: Point CSF at the Apache domain logs
Find where your server keeps per-domain access logs:
ls /usr/local/apache/domlogs/ | head
Then open /etc/csf/csf.conf with vi, find the CUSTOM1_LOG line and set it to those logs, for example:
CUSTOM1_LOG = "/usr/local/apache/domlogs/*"
Step 2: Add the blocking rules
Open /usr/local/csf/bin/regex.custom.pm with vi. Between the lines Do not edit before this point and Do not edit beyond this point, add:
# Block an IP after 5 requests to xmlrpc.php
if (($globlogs{CUSTOM1_LOG}{$lgfile}) and ($line =~ /^(\S+).*] "(?:GET|POST) \S*\/xmlrpc\.php/)) {
return ("WordPress XML-RPC attack",$1,"XMLRPC","5","80,443","1","0");
}
# Block an IP after 5 requests to wp-login.php
if (($globlogs{CUSTOM1_LOG}{$lgfile}) and ($line =~ /^(\S+).*] "POST \S*\/wp-login\.php/)) {
return ("WordPress login attack",$1,"WPLOGIN","5","80,443","1","0");
}
The values in each return line are: CSF
- the message lfd reports,
$1, the attacking IP address from the log line,- a unique name for the rule,
5, how many matches trigger a block (counted over CSF'sLF_INTERVAL, 3600 seconds by default),80,443, the ports to block,1, a permanent block (a larger number blocks for that many seconds instead, for example3600),0, whether to also block the IP at Cloudflare (1does that, if CSF's Cloudflare integration is set up).
The login rule only counts POST requests, so people simply opening the login page are not blocked.
Step 3: Restart CSF and lfd
csf -ra
If lfd does not start, there is a syntax error in regex.custom.pm; check /var/log/lfd.log.
If your sites are behind Cloudflare
Without extra setup, Apache logs Cloudflare's IP addresses instead of the visitors'. Cloudflare CSF would then block Cloudflare itself, taking sites offline. Before using Option 2 with Cloudflare:
- make Apache log the real visitor IP, using the
CF-Connecting-IPheader withmod_remoteip, as described in Cloudflare's guide; and - add Cloudflare's IP ranges to
/etc/csf/csf.ignore, so lfd never blocks them.
For sites behind Cloudflare, Option 1 is simpler and stops the requests before they reach the server at all.
Other protection
- Imunify360, if installed, has its own WordPress brute-force protection; see How to Install Imunify360.
- ModSecurity with the OWASP rules blocks many other attacks on WordPress; see How to Enable ModSecurity with the OWASP Core Rule Set. Older guides recommended Comodo's rules for this, which are no longer maintained.
- Strong, unique passwords and two-factor login for WordPress administrators make guessing pointless, whatever the bots try.