WordPress’s XML-RPC interface (xmlrpc.php) is a legacy remote-procedure-call API that predates the REST API — it allows external applications to create posts, manage comments, upload media, and check credentials via HTTP POST requests with XML payloads. Its system.multicall method is the most dangerous from a security perspective: a single HTTP request can bundle up to hundreds of authentication attempts as nested XML elements, turning a rate-limited brute-force attack into a multi-credential test with one network request per burst. WordPress enables XML-RPC by default and there is no admin toggle — disabling it requires either a PHP filter, server-level configuration, or both. The xmlrpc_enabled filter provides a WordPress-level disable: add_filter( ‘xmlrpc_enabled’, ‘__return_false’ ); in functions.php or a must-use plugin stops all XML-RPC authentication and method calls, returning an XML error response, but the request still reaches PHP-FPM — server-level blocking is more efficient because it never starts the PHP runtime. Server-level blocking at Nginx with location = /xmlrpc.php { return 403; } or at Apache with a <Files xmlrpc.php> Order deny,allow / Deny from all</Files> directive in .htaccess terminates the connection at the web server with no PHP involved. A pragmatic middle ground when XML-RPC is needed for specific clients (Jetpack, WP Mobile App): block system.multicall specifically while allowing other methods, and restrict XML-RPC access by IP using the xmlrpc_methods filter combined with server allowlists. Pingbacks — which use XML-RPC to notify external sites of links — are the most common legitimate use of XML-RPC for non-app installations; disabling XML-RPC entirely disables pingbacks, but the pre_option_enable_xmlrpc_multipleapicalls filter can disable only system.multicall without affecting pingbacks or Jetpack. The Application Passwords post provided the modern replacement for XML-RPC authentication; the Nginx rate limiting post covered request-rate control — both work in combination with disabling XML-RPC for a defence-in-depth posture.
Problem: Server access logs show thousands of POST requests to /xmlrpc.php per hour using the system.multicall method — each request tests 50–100 username/password pairs. The site does not use Jetpack, the mobile app, or any XML-RPC client. fail2ban is not catching these because the brute-force is spread across rotating IP addresses.
Solution: Block xmlrpc.php at the Nginx server level with a return 403 directive so PHP never runs, add a WordPress-level PHP filter as a defence-in-depth fallback, and remove the XML-RPC link from the document <head> to reduce automated scanner discoverability.
# Nginx: block xmlrpc.php completely — add inside server {} block
location = /xmlrpc.php {
# Return 403 Forbidden — PHP-FPM never starts, no WordPress bootstrap
return 403;
# Alternative: return 444 to drop the connection silently (no response)
# return 444;
}
# Also block common scanner probes for xmlrpc
location ~* ^/xmlrpc\.php {
return 403;
}
# Apache / .htaccess: block xmlrpc.php
<Files "xmlrpc.php">
# Apache 2.4+
Require all denied
# Apache 2.2 (legacy)
# Order deny,allow
# Deny from all
</Files>
// ── Must-use plugin: wp-content/mu-plugins/disable-xmlrpc.php ─────────────────
// Defence-in-depth: also disable at WordPress level in case server rules are bypassed
// Disable all XML-RPC functionality
add_filter( 'xmlrpc_enabled', '__return_false' );
// Disable system.multicall specifically (allows Jetpack while blocking multicall)
// Use this INSTEAD of the above if Jetpack is active
add_filter( 'xmlrpc_methods', function( array $methods ): array {
unset( $methods['system.multicall'] );
unset( $methods['system.listMethods'] ); // hides method inventory from scanners
unset( $methods['system.getCapabilities'] );
return $methods;
} );
// Remove XML-RPC discovery links from and HTTP headers
remove_action( 'wp_head', 'rsd_link' ); // Really Simple Discovery
remove_action( 'wp_head', 'wlwmanifest_link' ); // Windows Live Writer
remove_action( 'template_redirect', 'rest_output_link_header', 11 );
// Remove X-Pingback header (reveals XML-RPC endpoint to scanners)
add_filter( 'wp_headers', function( array $headers ): array {
unset( $headers['X-Pingback'] );
return $headers;
} );
// Disable trackbacks/pingbacks (sends and receives) without disabling XML-RPC entirely
add_filter( 'xmlrpc_methods', function( array $methods ): array {
// Remove only pingback-related methods — keep wp.* methods for app access
unset( $methods['pingback.ping'] );
unset( $methods['pingback.extensions.getPingbacks'] );
return $methods;
} );
# Verify xmlrpc.php is blocked — should return 403
curl -s -o /dev/null -w "%{http_code}" https://helloadmin.com/xmlrpc.php
# Test the multicall method specifically
curl -s -X POST https://helloadmin.com/xmlrpc.php -H "Content-Type: text/xml" -d 'system.multicall '
# Expected: 403 or empty body if server blocks it before PHP runs
# Check if X-Pingback header is removed
curl -sI https://helloadmin.com | grep -i x-pingback
# Expected: no output (header removed)
NOTE: If Jetpack is installed and connected, disabling XML-RPC entirely (xmlrpc_enabled => __return_false) breaks Jetpack synchronisation — Jetpack uses xmlrpc.php for its API calls. The correct approach for Jetpack sites is to block XML-RPC at the server level with IP allowlist rules that permit Jetpack’s server IPs while blocking all others, or to use only the PHP-level xmlrpc_methods filter to remove system.multicall while leaving the jetpack.* methods intact. Jetpack’s IP ranges are published at jetpack.com/support/how-to-add-jetpack-ips-allowlist. Also, WooCommerce’s mobile apps use the REST API (not XML-RPC) — blocking XML-RPC does not affect WooCommerce app functionality.