InfraFenceInfraFence

Live Security Operations

Get started

How to block xmlrpc.php brute force attacks on WordPress

Last updated:

xmlrpc.php is WordPress's old remote API. Few sites still need it, and every WordPress site on the internet gets attacked through it. Its system.multicall method runs many calls in a single request, so one POST can try hundreds of passwords, which is why login limiters that count requests to wp-login.php do not see it. Its pingback.ping method is used to make your site send requests to someone else's.

Below: how to recognize the attack in the access logs, how to block xmlrpc.php in nginx, Apache or WordPress itself, what to do if you use Jetpack, and how to rate-limit or ban with fail2ban when you must keep it open. Commands are for Debian and Ubuntu and for the RHEL family; example.com stands for your site.

Recognize an XML-RPC attack in the access logs

Count the POST requests to xmlrpc.php per address. A normal site sees a few a day, from Jetpack or an app. Hundreds from one address, or a steady stream from many, is an attack.

Any distribution
sudo sh -c "grep -hI 'POST /xmlrpc.php' /var/log/nginx/*access*.log /var/log/apache2/*access*.log \
  /var/log/httpd/*access_log /usr/local/apache/domlogs/* /var/log/apache2/domlogs/* 2>/dev/null" \
  | awk '{print $1}' | sort | uniq -c | sort -rn | head -20

The command reads the usual places: nginx, Apache on Debian and Ubuntu, httpd on the RHEL family, and cPanel's per-domain logs. The requests per hour show when it started:

Any distribution
sudo sh -c "grep -hI 'POST /xmlrpc.php' /var/log/nginx/access.log /var/log/apache2/access.log /var/log/httpd/access_log 2>/dev/null" \
  | awk '{print substr($4, 2, 14)}' | uniq -c | tail -n 24
  • The status is 200 even for a wrong password: XML-RPC reports the failure inside the response. Do not read 200 as a successful login.
  • Large responses, repeated from one address every few seconds, are typically system.multicall: one answer for many password attempts.
  • The log does not show which method was called: the request body is not logged. Brute force and pingback abuse look the same here.

To see whether XML-RPC answers on a site, ask it for its method list from any machine. A list of methods means it is on; a 403 means it is blocked.

Any machine with curl
curl -s -X POST -H 'Content-Type: text/xml' \
  -d '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName></methodCall>' \
  https://example.com/xmlrpc.php | head -c 400; echo

Block xmlrpc.php in nginx or Apache

Blocking at the web server is the cheapest option: the request is refused before PHP and WordPress load. In nginx, add this to the server block of each WordPress site:

nginx
# In the site's server { } block, before the "location ~ \.php$" block
location = /xmlrpc.php {
    deny all;
}
Any distribution
sudo nginx -t && sudo systemctl reload nginx

In Apache 2.4, put this in the site's .htaccess (no reload needed; LiteSpeed reads it too) or in its <VirtualHost>:

Apache
# In the site's .htaccess or <VirtualHost>
<Files "xmlrpc.php">
    Require all denied
</Files>
Debian / Ubuntu
sudo apachectl configtest && sudo systemctl reload apache2
RHEL, Rocky Linux, AlmaLinux, Oracle Linux, Fedora
sudo apachectl configtest && sudo systemctl reload httpd

Warning: Blocking xmlrpc.php breaks what still uses it: Jetpack (see below), pingbacks, and some older apps and publishing tools. If you are not sure, check the logs from step 1 for legitimate callers first.

Turn off XML-RPC inside WordPress

Where you cannot change the web server configuration, a must-use plugin does it from WordPress. Must-use plugins load on every request and cannot be turned off from the admin area by mistake. Replace the path with your site's:

Any distribution
sudo mkdir -p /var/www/example.com/wp-content/mu-plugins
sudo tee /var/www/example.com/wp-content/mu-plugins/disable-xmlrpc.php > /dev/null <<'EOF'
<?php
// Turns off the XML-RPC methods that need a login, and pingbacks.
add_filter( 'xmlrpc_enabled', '__return_false' );
add_filter( 'xmlrpc_methods', function ( $methods ) {
    unset( $methods['pingback.ping'], $methods['pingback.extensions.getPingbacks'] );
    return $methods;
} );
EOF

Note: The xmlrpc_enabled filter only turns off the methods that need a login, as the WordPress reference says; the second filter removes the pingback methods. PHP still runs for every request, so under a heavy attack the web server block above is lighter.

Plugins do the same: dedicated "Disable XML-RPC" plugins, and security plugins such as Wordfence and Solid Security have an option for it (as of November 2026). A plugin is easy to switch off by accident during an update; the mu-plugin and the web server rule are not.

Keep Jetpack working

Jetpack talks to your site through xmlrpc.php, so blocking it for everyone breaks the Jetpack connection. Jetpack publishes the address ranges its servers use on its allowlist page: allow those and block everyone else. In nginx the location must then also pass the request to PHP:

nginx
location = /xmlrpc.php {
    # Jetpack's ranges, from its allowlist page (check it for changes)
    allow 122.248.245.244/32;
    allow 54.217.201.243/32;
    allow 54.232.116.4/32;
    allow 192.0.64.0/18;
    allow 195.234.108.0/22;
    deny all;
    # then the same lines as in your "location ~ \.php$" block, for example:
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass unix:/run/php/php-fpm.sock;
}

Copy the PHP lines from the site's own location ~ \.php$ block: the socket path differs between systems. In Apache, Require ip alone allows only those ranges:

Apache
<Files "xmlrpc.php">
    Require ip 122.248.245.244/32 54.217.201.243/32 54.232.116.4/32 192.0.64.0/18 195.234.108.0/22
</Files>

Note: Ranges as listed by Jetpack in November 2026. They can change: check the page from time to time. Behind Cloudflare or another proxy, these rules see the proxy's address unless the real visitor address is restored first (nginx real_ip, Apache mod_remoteip).

Rate-limit xmlrpc.php when it must stay open

If an app or service you cannot list by address needs XML-RPC, limit how often each address may call it. In nginx, define a zone in the http context, then use it for xmlrpc.php: here 6 requests a minute per address, with a burst of 5, and 429 beyond that.

nginx
# /etc/nginx/conf.d/xmlrpc-limit.conf (read inside the http { } block)
limit_req_zone $binary_remote_addr zone=xmlrpc:10m rate=6r/m;
nginx
# In the site's server { } block
location = /xmlrpc.php {
    limit_req zone=xmlrpc burst=5 nodelay;
    limit_req_status 429;
    # then the same lines as in your "location ~ \.php$" block, for example:
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass unix:/run/php/php-fpm.sock;
}
Any distribution
sudo nginx -t && sudo systemctl reload nginx

Note: A rate limit counts requests, not passwords: one system.multicall request inside the limit can still carry hundreds of attempts. Pair it with strong passwords, or ban the addresses with fail2ban. Apache has no built-in equivalent; use fail2ban below.

Ban XML-RPC attackers with fail2ban

fail2ban reads the access log and bans an address at the firewall, so it works with nginx and Apache alike. Install it as in How to stop SSH brute force attacks (Debian and Ubuntu repositories, EPEL on the RHEL family). Then create a filter for POST requests to xmlrpc.php:

Any distribution
sudo tee /etc/fail2ban/filter.d/wordpress-xmlrpc.conf > /dev/null <<'EOF'
[Definition]
failregex = ^<HOST> \S+ \S+ \[[^]]+\] "POST /xmlrpc\.php
ignoreregex =
EOF

And a jail: 5 requests in 60 seconds, a ban of one hour. Put your own address in ignoreip, and, if you use Jetpack, its ranges. For Apache, set logpath to /var/log/apache2/*access.log on Debian and Ubuntu or /var/log/httpd/*access_log on the RHEL family.

Any distribution
sudo tee /etc/fail2ban/jail.d/wordpress-xmlrpc.local > /dev/null <<'EOF'
[wordpress-xmlrpc]
enabled  = true
port     = http,https
filter   = wordpress-xmlrpc
logpath  = /var/log/nginx/access.log
maxretry = 5
findtime = 60
bantime  = 3600
ignoreip = 127.0.0.1/8 ::1 192.0.2.10
EOF

Test the filter on the real log before you enable it: fail2ban-regex shows how many lines match. Then restart fail2ban and check the jail.

Any distribution
sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/wordpress-xmlrpc.conf
sudo systemctl restart fail2ban
sudo fail2ban-client status wordpress-xmlrpc

Warning: Behind Cloudflare or another proxy, the access log shows the proxy's address: fail2ban would ban the proxy and take the site offline for many visitors. Restore the real address in the log first, or block at the proxy.

How InfraFence handles XML-RPC attacks

InfraFence's web firewall reads the access logs of nginx, Apache and LiteSpeed, and from agent 1.0.42 also of Traefik, Caddy, HAProxy and web servers in containers. Behind Cloudflare it takes the visitor address from CF-Connecting-IP, and only from Cloudflare's own ranges.

  • Every 5 POST requests to xmlrpc.php from one address on one site within a minute add 25 points to that address's score. A sustained attack keeps adding them.
  • The score also collects the address's other attacks, such as brute force on wp-login.php, plugin scanning or exploit attempts, and decays by 5 points a minute, so a single odd request fades away.
  • At 80 points the address is banned at the firewall and the event is reported as critical. Thresholds and points can be changed per server, and monitor mode reports without banning.

See Web firewall for the rules and the score. What it does not do: it counts requests, so it cannot see how many passwords a system.multicall request carries; it does not change your nginx, Apache or WordPress configuration, so xmlrpc.php stays open until you block it as above; and it has no built-in exception for Jetpack: if Jetpack's servers call often enough to be scored, add their ranges to the whitelist. The ban comes from the log, so the request that triggered it has already been answered.

Questions

Is it safe to disable xmlrpc.php?
For most sites, yes: the block editor and the admin area use the REST API, not XML-RPC. Check first whether you use Jetpack, pingbacks, an app or a publishing tool that still relies on it.
Why do the attacks show status 200?
XML-RPC answers 200 and reports the failed login inside the response. The status code says nothing about whether a password worked.
My login limiter plugin does not stop it. Why?
Many limiters count attempts on wp-login.php. A system.multicall request through xmlrpc.php carries hundreds of attempts in one request and goes around them. Block or limit xmlrpc.php itself.

Try it free on 1 server

Starter is free for one server, with full protection and automatic blocking. No credit card.

Install the agent as root, with your token in place of <TOKEN>:

curl -fsSL https://github.com/infrafence/infrafence-agent/releases/latest/download/install.sh | sudo bash -s -- --token <TOKEN>

The token is created in the dashboard, in Servers › Add server.