Disable and Secure WordPress xmlrpc.php Against Brute-Force Attacks

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.