This check follows the xmlrpc check: even when a plugin or mu-plugin disables XML-RPC logically (the xmlrpc_enabled filter), the file xmlrpc.php still loads and PHP still runs. The check simply asks: I sent a request to the file, and got a 200 or 405 response — not 403 or 404. The file is still reachable.
Why this matters
A file that returns 405 or 200 is a file the server is willing to serve. That means even when WordPress refuses to execute the requested method, it does so after a full HTTP request: PHP boots, the WordPress core loads, the database connection opens, and only then is the request rejected. An attacker running request-amplification techniques with a large-scale automated attacks attempts triggers a thousand full PHP boots. Even without successful authentication, this is heavy CPU consumption — effectively a DoS. And any future RCE in xmlrpc.php still executes because the file is loaded. A server-level block neutralizes both: the server returns 403 before PHP starts.
How to detect
curl -I https://example.com/xmlrpc.php
One of two outcomes:
- Properly blocked:
HTTP/1.1 403 ForbiddenorHTTP/1.1 404 Not Found - Vulnerable:
HTTP/1.1 200 OKorHTTP/1.1 405 Method Not Allowed
A deeper POST probe:
curl -X POST -d '<methodCall><methodName>system.listMethods</methodName></methodCall>' \
-w "Status: %{http_code}\n" \
https://example.com/xmlrpc.php
403 is good. An XML body with a method list, or "XML-RPC services are disabled" inside an XML envelope, means the file still loads — vulnerable.
How to fix
Apache — add to root .htaccess:
<Files xmlrpc.php>
Require all denied
</Files>
Nginx — add to the server block (not htaccess; Nginx ignores htaccess):
location = /xmlrpc.php {
deny all;
access_log off;
log_not_found off;
}
access_log off and log_not_found off matter: without them, the access log fills quickly with attack attempts. With the server returning 403 silently, the attack leaves no trace.
LiteSpeed (mostly Apache-compatible, but also via LSWS):
<Files xmlrpc.php>
Require all denied
</Files>
Cloudflare (in front of origin): Security > WAF > Custom rules. Create a rule: (http.request.uri.path eq "/xmlrpc.php") with action Block. This blocks at the edge before the request ever reaches the server.
If a specific plugin needs XML-RPC (Jetpack in older configs), allow only authorized IP ranges:
<Files xmlrpc.php>
Require ip 192.0.64.0/18
Require ip 76.74.255.0/24
</Files>
Common mistakes
Mistake one: trusting a "Disable XML-RPC" plugin. Most only add a PHP filter; they do not block at the web server layer. After the plugin is active, the file still returns 200 or 405 instead of 403. Always verify with curl. Mistake two: putting the rules in .htaccess on an Nginx server. Nginx does not read htaccess — your rules sit there with no effect. Ask the host which server is actually serving requests. Mistake three: blocking at origin without addressing a fronting WAF/CDN. If Cloudflare passes the request to origin and the origin blocks it, that is functional — but ideally block at the edge to save bandwidth and CPU. Mistake four: testing right after the change instead of after 24 hours. A page cache like LiteSpeed Cache sometimes stores xmlrpc.php responses; the first curl will show 200 even after blocking until the cache expires.
Verifying the fix
Run both curl commands from "How to detect" again. Both must return 403 (Apache/Cloudflare) or 444/404 (Nginx). If you still see 200 or 405, the rule did not load. Verify config: apachectl configtest or nginx -t, then systemctl reload apache2 or nginx -s reload. Wait five minutes and retest. Finish by running the audit and confirming both xmlrpc and xmlrpc_reachable pass.