Guide
WordPress xmlrpc.php under attack — detecting amplification and credential stuffing
1. Problem
Your access log is suddenly full of POST /xmlrpc.php. Thousands of them. Same endpoint, hundreds of IPs, no obvious pattern in the user-agent. The site is slow but not down. You see wordpress xmlrpc.php attacks flooding logs everywhere on Stack Overflow and the answers all say "just disable it" — but you have a Jetpack-connected site and you're not sure what breaks if you do.
Then you check the auth log and see something stranger: 40,000 failed login attempts in the last hour, but only a handful of wp-login.php POSTs. The brute force is happening somewhere else.
It's xmlrpc.php. And it's not just a flood — it's an amplification attack. One production WordPress entity in our fleet logged 21,865 XML-RPC requests in 7 days from a single attack window. Most of them were system.multicall payloads, each one carrying a batch of wp.getUsersBlogs credential probes. One HTTP request, hundreds of password attempts inside it.
This guide is about catching that attack early, distinguishing it from the more familiar REST API credential stuffing, and deciding whether to block, harden, or kill XML-RPC entirely.
2. Impact
XML-RPC abuse is the most underestimated WordPress attack surface in 2026. Three concrete reasons:
1. It bypasses every login rate-limiter you have. Plugins that throttle wp-login.php see one request and ignore it. That one request just tested 1,000 passwords.
2. It amplifies bandwidth. The pingback method (pingback.ping) turns your site into a DDoS reflector. Attackers send a small XML-RPC request to your xmlrpc.php and you make a large HTTP request to their target. Your server eats the egress bill.
3. It hides credential success. When system.multicall finally hits a valid password, the response looks identical in the access log to all the failures — same 200 status, same response size give-or-take. The compromise gets buried in the flood.
The downstream cost is real: stolen accounts, account-takeover spam, your IP getting listed for outbound DDoS, and CPU saturation that takes legit traffic down with the abuse.
3. Why It’s Hard to Spot
WordPress core has zero observability around XML-RPC. There is no admin notification when xmlrpc.php is hammered. There is no failed-login dashboard that includes XML-RPC auth attempts. The Site Health screen will not warn you. Most security plugins log wp-login.php only.
Your access log technically captures it, but the request is a single POST /xmlrpc.php line — the log doesn't know that line contained 1,000 password attempts. Without parsing the XML body (which access logs don't do), you cannot distinguish a legit Jetpack sync from a credential-stuffing flood.
CDN and uptime monitors miss it for a different reason: the site stays up. Response codes stay 200. Latency degrades but rarely crosses page-load alert thresholds. The attack is designed to look like normal write traffic.
The combined effect: the only place the truth lives is inside the XML body of the request, and nothing in the default stack reads it. This is the textbook silent failure shape — high-volume, log-visible, dashboard-invisible.
4. Cause
The wp_xmlrpc_requests_total signal is emitted by the WordPress plugin every time the xmlrpc.php endpoint is invoked, with a label for the XML-RPC method name parsed from the request body. This is the key detail: the access log only shows POST /xmlrpc.php. The signal shows you system.multicall, pingback.ping, wp.getUsersBlogs, metaWeblog.newPost — the actual instruction.
There are three abuse patterns to distinguish:
Pattern A — system.multicall credential stuffing. Attacker POSTs one request containing an XML payload with N nested calls, typically wp.getUsersBlogs(username, password) or wp.getProfile(username, password). Each nested call is a credential test. The standard payload size is 500–2000 attempts per HTTP request. WordPress processes them in-process and returns one response with N results. You see wp_xmlrpc_requests_total{method="system.multicall"} climb slowly, but wp_auth_attempts_total{result="failed"} climbs fast — sometimes 1000:1 ratio. That ratio is the signature.
Pattern B — Pingback DDoS reflection. Attacker sends pingback.ping with sourceURI pointing at a victim and targetURI pointing at a post on your site. Your site fetches the source URI to verify the pingback — meaning your server makes a large outbound HTTP request to the attacker's chosen target. wp_xmlrpc_requests_total{method="pingback.ping"} rises with diverse target URLs, and you see outbound network spikes. The fingerprint is method=pingback.ping from many distinct source IPs hitting the same victim domain.
Pattern C — Direct method enumeration. Lower volume, recon phase. system.listMethods, system.getCapabilities. If you see these on a site that doesn't normally publish via XML-RPC, someone is mapping you for one of the above attacks.
The Logystera processor matches these per-method, applies per-entity rate thresholds, and emits a security signal when any single method crosses its baseline. The label cardinality is intentionally bounded — XML-RPC has a finite method namespace.
5. Solution
5.1 Diagnose (logs first)
Start by confirming the volume on the access log, then move to signal-level analysis.
Step 1 — Confirm the flood. On the WordPress host:
grep 'POST /xmlrpc.php' /var/log/nginx/access.log \
| awk '{print $1}' | sort | uniq -c | sort -rn | head -20
If the top IPs show hundreds-to-thousands of requests in a single log file, you have a flood. If the IP distribution is wide (200+ unique IPs each making 50–200 requests), you have a botnet — that pattern feeds wp_bot_requests_total once Logystera fingerprints the traffic.
Step 2 — Read the XML body to identify the method. Access logs don't capture POST bodies, so enable nginx body capture temporarily or use tcpdump:
sudo tcpdump -A -s 0 -i any 'tcp port 80 and host www.example.com' \
| grep -A 5 'methodName'
Look for the element. If it's system.multicall, you are seeing Pattern A. If it's pingback.ping, Pattern B. Each occurrence emits wp_xmlrpc_requests_total with the matching method label.
Step 3 — Correlate with auth attempts. This is the load-bearing query. If you see wp_xmlrpc_requests_total{method="system.multicall"} of 200/min and wp_auth_attempts_total{result="failed"} of 80,000/min on the same entity, the multicalls are wrapping credential tests. The ratio between these two signals is the credential-stuffing fingerprint:
wp_auth_attempts_total{result="failed"} / wp_xmlrpc_requests_total{method="system.multicall"} > 100
In the production incident that prompted this guide — the 21,865-request entity — the ratio was 412 failed auth attempts per multicall. That site was getting roughly 9 million credential probes hidden inside 22k visible HTTP requests.
Step 4 — Check for the success. The dangerous moment is when one of those probes succeeds. Filter for wp_auth_attempts_total{result="success"} events that occurred within 60 seconds of a system.multicall request from the same source fingerprint. The wp_request_fingerprint_top signal collapses IP+UA+TLS-JA3 into stable identifiers; pivoting on it makes the source visible even when IPs rotate.
Step 5 — Audit pingback abuse. For Pattern B:
grep 'POST /xmlrpc.php' /var/log/nginx/access.log \
| awk '{print $7, $1}' | sort | uniq -c | head
Then sample a few request bodies. If is present and the values point to many different external domains, you are being used as a reflector. This emits wp_xmlrpc_requests_total{method="pingback.ping"} with diverse downstream targets.
5.2 Root Causes
(see root causes inline in 5.3 Fix)
5.3 Fix
Blocking is the easy part. Knowing which block is safe for your site is the hard part. Decide in this order:
Option 1 — Hard-disable XML-RPC (most sites). If you don't use Jetpack, the WordPress mobile app, IFTTT, or any third-party publishing tool, kill it. Drop in mu-plugins/disable-xmlrpc.php:
<?php
add_filter('xmlrpc_enabled', '__return_false');
add_filter('wp_xmlrpc_methods', '__return_empty_array');
Then return 403 at the web server layer:
location = /xmlrpc.php { deny all; return 403; }
After this, wp_xmlrpc_requests_total should flatline at zero. If it doesn't, your nginx config didn't take.
Option 2 — Disable system.multicall and pingback.ping, keep the rest (Jetpack sites). Jetpack uses XML-RPC but does not require multicall or pingbacks. This is the safe middle path:
add_filter('xmlrpc_methods', function ($methods) {
unset($methods['system.multicall']);
unset($methods['pingback.ping']);
unset($methods['pingback.extensions.getPingbacks']);
return $methods;
});
This kills Pattern A and Pattern B simultaneously while preserving Jetpack sync. Verify Jetpack still connects after deploying — wp jetpack status from WP-CLI.
Option 3 — Rate-limit at the edge. If you must keep multicall (rare, but some legacy publishing flows need it), throttle hard:
limit_req_zone $binary_remote_addr zone=xmlrpc:10m rate=2r/s;
location = /xmlrpc.php {
limit_req zone=xmlrpc burst=5 nodelay;
fastcgi_pass ...;
}
Two requests per second per IP makes credential stuffing economically unattractive without breaking legit clients.
Cause-to-signal map for this section:
system.multicallflood from botnet →wp_xmlrpc_requests_total{method="system.multicall"}pluswp_auth_attempts_total{result="failed"}ratio above 100 → Pattern A.pingback.pingreflection abuse →wp_xmlrpc_requests_total{method="pingback.ping"}with diversesourceURItargets and outbound bandwidth spike → Pattern B.- Recon enumeration →
wp_xmlrpc_requests_total{method="system.listMethods"}from a single fingerprint → Pattern C, expect Pattern A or B within 24 hours.
Pick the option that matches your integration footprint. If unsure, Option 2 is correct for the majority of production WordPress sites in 2026.
5.4 Verify
The verification is signal-shaped, not status-code-shaped.
Within 5 minutes of deploying the fix, wp_xmlrpc_requests_total for the abused method should drop to zero (Options 1 and 2) or to your rate-limit ceiling (Option 3). If you blocked at nginx, you should see 403s in the access log, but the WordPress-side signal should stop emitting because the request never reaches PHP.
Within 30 minutes, wp_auth_attempts_total{result="failed"} should fall by an order of magnitude on that entity. If it doesn't, the attacker has shifted to wp-login.php or the REST API — which is the next guide in this cluster.
Healthy steady state looks like this in logs:
# Should return zero or near-zero on a hard-disabled site:
grep 'POST /xmlrpc.php' /var/log/nginx/access.log | wc -l
# Should match Jetpack heartbeat cadence only (a handful per hour):
# on a Jetpack-connected site after Option 2.
The pattern that should disappear: long runs of POST /xmlrpc.php 200 from rotating IPs. The pattern that should remain (if Jetpack): occasional POST /xmlrpc.php 200 from Automattic's IP ranges, low frequency, single-method calls.
If after 24 hours wp_xmlrpc_requests_total is flat and wp_auth_attempts_total{result="failed"} is at baseline, the abuse vector is closed.
6. How to Catch This Early
Fixing it is straightforward once you know the cause. The hard part is knowing it happened at all.
This issue surfaces as wp_xmlrpc_requests_total.
XML-RPC abuse is the model case for log-derived security signals. The attack is designed to look normal at every layer above the request body — uptime is fine, status codes are fine, page-load times degrade slowly. The only place the truth lives is in the method name inside the XML payload, and nothing in the standard WordPress, nginx, or CDN stack surfaces it.
This is exactly the gap Logystera closes. The WP plugin parses the XML-RPC body server-side and emits wp_xmlrpc_requests_total with the method label, plus the correlated wp_auth_attempts_total for credential probes nested inside multicalls. The processor evaluates per-entity baselines and triggers when method-volume crosses threshold or when the multicall-to-failed-auth ratio indicates credential stuffing.
The result: instead of finding 21,865 unexplained xmlrpc.php requests in your logs after a week, you get an alert in the first 90 seconds — naming the method, the fingerprint, and whether it's amplification (Pattern B) or credential stuffing (Pattern A). You decide which block to deploy with the data already in hand.
The point isn't that Logystera detects it. The point is that without log-body parsing, nothing detects it. Your hosting provider sees normal traffic. Your CDN sees normal traffic. Your APM sees normal traffic. The signal lives one layer below where they look.
7. Related Silent Failures
If wp_xmlrpc_requests_total is firing on your fleet, these adjacent failure modes are statistically likely. They share the credential-stuffing or amplification signature from a different angle:
- REST API credential stuffing —
wp_auth_attempts_totalrising against/wp-json/wp/v2/usersenumeration. Different log signature, same goal. Covered inwordpress/rest-api-credential-stuffing. wp-login.phpbrute force — the classic. If you blocked XML-RPC, traffic often migrates here within hours. Watchwp_login_attempts_total{endpoint="wp-login"}.- User enumeration via author archives —
/?author=1,/?author=2scanning. Emitswp_user_enum_total. Usually precedes a credential-stuffing wave by 24–48 hours. - Pingback-based outbound DDoS — your site as the source, not the target. Detected via
wp_outbound_requests_totalcorrelated withwp_xmlrpc_requests_total{method="pingback.ping"}. - Plugin-induced auth bypass — silent role escalation events. Emits
wp_state_changeon user role transitions. Often the post-compromise step after a successful credential probe.
The cluster is auth + abuse. Once one signal in this group fires, the others should be on your watch list for the following week.
See what's actually happening in your WordPress system
Connect your site. Logystera starts monitoring within minutes.