WordPress Security

Stopping Brute-Force and XML-RPC Attacks on WordPress

How to recognize password-guessing traffic in your access log, rate limit wp-login.php before PHP runs, decide whether xmlrpc.php can be blocked, and what user enumeration is really worth.

Stopping Brute-Force and XML-RPC Attacks on WordPress

A site that slows down at random hours, a host emailing about CPU limits, a security plugin reporting thousands of blocked logins: these are the usual signs of automated password guessing, and the reason people go looking for WordPress brute force protection. The requests are cheap to send and expensive for your server to answer, because every one of them starts PHP and queries the database.

That protection has two jobs. One is to make guessing pointless, which is the work of unique passwords and a second factor. The other is to make each guess cheap to reject, which is the work of rate limits placed as far from PHP as possible. This guide deals mostly with the second job and with xmlrpc.php, the endpoint people forget.

Confirm it in the access log

Before changing anything, look at what is arriving. On managed hosting the access log is in the control panel, often under "Logs" or "Raw access". On your own server it is usually /var/log/nginx/access.log or /var/log/apache2/access.log. Three patterns matter:

  • Repeated POST /wp-login.php lines. A failed login is answered with status 200, because WordPress shows the form again. A successful one is answered with 302, a redirect to the dashboard. A long run of 200s is guessing. A 302 in the middle of it, from an address you do not know, is the line to worry about.
  • Repeated POST /xmlrpc.php lines on a site where you use nothing that talks to that file.
  • Requests for /?author=1, /?author=2 and so on, or for /wp-json/wp/v2/users. Something is collecting usernames.

Two commands turn the log into numbers:

grep "POST /wp-login.php" access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -20
grep -c "POST /xmlrpc.php" access.log

The first lists the addresses that posted to the login page most often. A few addresses with thousands of requests each are easy to stop with a per-address limit. Thousands of addresses with a few requests each are a distributed attack, and for those a per-address limit helps less than a challenge at the edge and a second factor on the accounts. If the site sits behind a CDN, make sure the log records the visitor's address and not the CDN's, or every line will show the same source.

Reject the requests before PHP runs

There are three places to put a limit, and the official WordPress guidance on brute-force attacks ranks them the same way we do.

At the edge

A CDN or web application firewall can rate limit or challenge requests to /wp-login.php and /xmlrpc.php before they reach your server at all. This is the best option under heavy load, since the blocked traffic costs you nothing.

At the web server

On nginx, the request limiting module does the job in a few lines:

# In the http block
limit_req_zone $binary_remote_addr zone=wplogin:10m rate=10r/m;

# In the server block
location = /wp-login.php {
    limit_req zone=wplogin burst=5 nodelay;
    limit_req_status 429;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass unix:/run/php/php-fpm.sock;
}

This location takes over from your general PHP block for that one file, so it has to hand the request to PHP itself. Copy the fastcgi lines from your existing PHP location, because the socket path differs from server to server. Run nginx -t before reloading. Ten requests a minute with a small burst is far more than a person needs. Raise it if a whole office logs in from one address.

Apache has no equally simple built-in limiter. If staff always connect from fixed addresses or a VPN, restrict the file to them:

<Files "wp-login.php">
    Require ip 203.0.113.15
</Files>

Do not use that rule on a site where customers or members log in or reset passwords through wp-login.php. On a server where you have root access, a log-watching tool such as Fail2ban can ban addresses at the firewall after repeated failures.

Inside WordPress

A plugin that limits login attempts is the fallback when you cannot touch the server. It works, but PHP still starts for every attempt it turns away, so it protects accounts better than it protects performance.

xmlrpc.php: what still uses it and when to block it

XML-RPC is the remote publishing interface WordPress had before the REST API. It accepts a username and password with each call, and because nobody is present to type a code, two-factor plugins do not apply to it. The official guidance calls it a frequent brute-force target and is direct about the remedy: if you do not use XML-RPC, disable it.

So the real question is whether you use it. The things that have depended on it are Jetpack's connection to WordPress.com, older versions of the mobile apps and desktop publishing tools, some automation services, and pingbacks between blogs. To find out for your site, search the access log for POST /xmlrpc.php and see whether any of the callers is a service you recognize.

If nothing needs it, block the file at the web server:

# Apache 2.4, in .htaccess
<Files "xmlrpc.php">
    Require all denied
</Files>

# nginx, in the server block
location = /xmlrpc.php {
    deny all;
}

You will see a PHP alternative in many tutorials:

add_filter( 'xmlrpc_enabled', '__return_false' );

It is weaker than it looks. According to the hook's own documentation, it only switches off the methods that require a login. Pingbacks keep working, and PHP still runs for every request. Use it when the server block is not available to you.

If you do need XML-RPC, for Jetpack as the common case, leave it open and protect it instead. Apply the same rate limit you gave the login page, and if your firewall can restrict by source, allow only the addresses the service publishes for this purpose. You can also untick "Allow link notifications from other blogs (pingbacks and trackbacks)" under Settings > Discussion if you do not want pingbacks.

User enumeration: worth knowing, not worth chasing

WordPress will tell anyone who asks which usernames have published content. An author archive at /?author=1 redirects to an address containing the username, and the REST API lists authors. The WordPress project does not treat usernames as secret, and it is right not to. A login that is safe only while the username stays hidden is not safe.

Some low-effort steps are still reasonable. Do not use admin, the company name or the domain as an administrator username. Keep administrator accounts for administration, so they never appear as authors. A firewall rule that drops author= scans will tidy the logs. Be careful with plugins that switch off the users endpoint entirely, because the block editor needs it for logged-in staff.

What rate limits cannot stop

Limits slow down guessing. They do nothing when the attacker already has the right password, taken from a breach at another service where the same person reused it. That login succeeds on the first try and trips no limit.

This is why the first job matters more than the second. Every account that can publish or administer needs a unique password and a second factor, and application passwords need reviewing, since they skip the second factor by design. The methods and the rollout are in our guide to securing the WordPress login.

Check that it worked

  • From a connection that is not on any allowlist, such as a phone on mobile data, submit a wrong password to your own site a dozen times in a row. You should receive a 429, a 403 or a challenge.
  • Log in normally, then run a password reset from start to finish.
  • If the site is a store or a membership site, test the customer login and registration pages as well.
  • Run curl -I https://example.com/xmlrpc.php with your own domain. A blocked file answers 403. An open one answers 405, because it only accepts POST requests.
  • Test Jetpack or the mobile app if you kept XML-RPC open for them.
  • A week later, run the two log commands again and compare.

WordPress brute force protection when the server is not yours

On shared hosting you cannot edit the nginx configuration, and that is fine. A firewall or CDN in front of the site plus a login-limiting plugin covers most of what the server rules do. Managed WordPress hosts often rate limit logins and block XML-RPC already, so ask what is in place before adding your own layer on top.

Bring in help when the attack is distributed and is affecting uptime, when the login page is customer-facing and a wrong rule would lock out buyers, or when apps and integrations sign in through endpoints of their own. Tuning those limits and watching the logs afterwards is part of a WordPress maintenance plan. If the load has also exposed a slow server, our performance optimization service deals with that side.

Frequently asked questions

Can I just delete xmlrpc.php?

No. It is a core file, so the next WordPress update puts it back, and file integrity checks will report it as missing in the meantime. Block access to it at the web server, which survives updates and costs nothing to undo.

How many login attempts should I allow before a lockout?

There is no magic number. Something like five to ten failures from one address within a few minutes, followed by a short lockout that grows on repeat, stops scripts without punishing a person who mistyped. Avoid permanent bans, and avoid lockouts based only on the username, which let a stranger lock you out.

Does changing the login address stop brute-force attacks?

It reduces the traffic to wp-login.php, because simple bots only try the default address. It does not protect xmlrpc.php or the REST API, and it does nothing against a stolen password. Treat it as a way to quiet the logs, not as protection.

Why is my small site being attacked at all?

Because nobody chose it. The scripts work through lists of every WordPress site they can find and try each one, so the size or fame of the site is irrelevant.

Add WorldWin Coder to your preferred sources

Add worldwincoder.com as a preferred source on Google, or open this article in your AI assistant to use it as a source.

Swati Priyadrshni
· Founder & CEO at WorldWin Coder

I'm Swati Priyadarshani, Founder & CEO of LMS Advisor and Co-Founder of WorldWin Coder Pvt. Ltd. (est. 2019). Over the last 10+ years, I've helped enterprises build learning platforms that actually get used - not just deployed.

Keep reading

Have something to build, fix or move?

Tell us where you are. A developer reads every inquiry, and you get next steps and a free quote within one business day.