Block XML-RPC password logins in WordPress with the authenticate filter

Block XML-RPC Password Logins in WordPress (and What It Means for Jetpack)

Hiding your WordPress login page does not close every door to a password guess. xmlrpc.php is a second login endpoint: it takes a username and password with each call, it does not use the login page, and bots probe it routinely. The usual advice is to disable XML-RPC completely, but that can break Jetpack and some publishing clients. This guide shows a targeted alternative that I run on two WordPress 7.1.3 sites: leave the endpoint in place, refuse any login that succeeds over XML-RPC, and remove the pingback methods. It takes one small must-use plugin. Because “does it break Jetpack?” is the obvious worry, I checked Jetpack’s own code, and the result is below.

Why xmlrpc.php matters

XML-RPC is an older remote interface that WordPress still ships. A client calls a method such as wp.getUsersBlogs and puts a username and password in the request, so the endpoint can answer “right” or “wrong” without anyone using the login form. Plugins that hide or rename wp-login.php do not change that, and a login-page lockout may not see these attempts. Search your access log for requests to /xmlrpc.php and you will probably find some.

Your options

  • Block xmlrpc.php at the web server. The simplest and strongest option if nothing uses it, and better than deleting the file, because a WordPress update can put a core file back. WordPress VIP’s documentation says most of Jetpack’s features rely on the XML-RPC connection, so this approach breaks Jetpack.
  • Turn XML-RPC off with the xmlrpc_enabled filter. It affects the methods that need a login. Methods that do not need one, such as pingbacks, are still available unless you remove them. Reports about what it does to Jetpack conflict, and I did not use it, so I have not tested it.
  • Refuse logins that succeed over XML-RPC (this guide). The endpoint stays available, but no username-and-password login can authenticate a request through it.

The plugin

Create wp-content/mu-plugins/xmlrpc-hardening.php. Must-use plugins load automatically and do not appear in the normal plugin list:

<?php
/**
 * Plugin Name: XML-RPC hardening
 * Description: Refuses logins over XML-RPC and removes pingback.
 */

// Refuse any login that succeeds during an XML-RPC request.
add_filter('authenticate', function ($user) {
    if (defined('XMLRPC_REQUEST') && XMLRPC_REQUEST && $user instanceof WP_User) {
        return new WP_Error('xmlrpc_password_auth_disabled',
            'Password authentication over XML-RPC is disabled on this site.');
    }
    return $user;
}, 999);

// Pingbacks are a common abuse vector.
add_filter('xmlrpc_methods', function ($methods) {
    unset($methods['pingback.ping'], $methods['pingback.extensions.getPingbacks']);
    return $methods;
});
add_filter('wp_headers', function ($headers) {
    unset($headers['X-Pingback']);
    return $headers;
});

Why priority 999

The authenticate filter passes a result along a chain of callbacks, and each one sees what the earlier ones returned. The WordPress documentation lists the core username-and-password check on that filter at priority 20, and the application-password check at the same priority. A callback at priority 999 runs after them, so it sees a successful login and can turn it into an error. A first version of mine used priority 1 and did nothing for correct credentials: the core check ran afterwards and returned a valid user.

The rule is deliberately blunt: during an XML-RPC request, any user that arrives at priority 999 is refused, whichever plugin authenticated it. The constant XMLRPC_REQUEST is only defined for XML-RPC requests, so logins through the login page and the REST API are not affected.

The pingback lines remove two methods that can be used to make your site send requests elsewhere, and stop the header that advertises the pingback address.

Will it break Jetpack?

I wanted to know before relying on this. Jetpack’s connection package handles its own signed requests through a special path. In the Jetpack Social 10.0.0 plugin installed here (I first read 9.0.3, and the relevant code is unchanged), the method that sets up that path says “Don’t let anyone authenticate”, calls remove_all_filters('authenticate'), and then registers Jetpack’s own authentication callback. That removes every other callback on the filter, including this plugin’s rule, so for those requests the rule never runs. This is my reading of that version’s code, not a statement from Jetpack, and it can change between versions. The connection indicator in the Jetpack dashboard is stored state and does not prove that every feature works, so after adding the plugin test the Jetpack features you use, for example by publishing a test post and checking that it reaches the service you connected.

Check that it works

Ask the endpoint which methods it offers and look for pingback:

curl -s -H "Content-Type: text/xml" \
  --data '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName></methodCall>' \
  https://example.com/xmlrpc.php

On my site the list had 78 methods and none of them contained “pingback”, and the X-Pingback header was gone from the home page response (curl -sI https://example.com/ | grep -i x-pingback printed nothing).

To test the password block, create a temporary low-privilege user with a throwaway password (a password typed on a command line can end up in shell history), call wp.getUsersBlogs with it, and delete the user afterwards. WordPress replaces the error from a failed login with a generic XML-RPC fault, so look in the XML body for the fault code 403 and the text “Incorrect username or password.” That is the same answer a wrong password gets, and it usually arrives inside an HTTP 200 response. Before the plugin, the same call with the correct password returns the list of blogs. I have not tested application passwords, but they are checked at the same priority as ordinary passwords, so the same rule should refuse them.

What this does and does not do

  • It prevents a successful login from authenticating a request through XML-RPC, including a correct guess.
  • It does not stop the requests from arriving. WordPress still processes them and checks the credentials until the check fails, which costs PHP time under heavy attack, and I have not measured how much. Rate-limit these requests at the edge, for example with a Cloudflare rule, and use a security plugin such as Wordfence for application-level protection. Do not block Jetpack’s traffic if you use it.
  • It breaks any client that logs in over XML-RPC, including older publishing tools, some mobile apps and, for the reason above, application passwords used over XML-RPC.

If something goes wrong

A PHP error in a must-use plugin can take the whole site down, and you cannot fix it from the dashboard. Keep file access ready (SFTP, SSH, or docker exec for a container) before you add it. To switch it off, move the file out of wp-content/mu-plugins/ or rename it so that it no longer ends in .php. Nothing is written to the database.

Other layers worth having

  • Strong, unique passwords and two-factor authentication for every administrator account.
  • Login attempt limits from a security plugin, and a rate-limit rule for repeated requests to xmlrpc.php and wp-login.php.
  • Updates for WordPress and its plugins.

Common questions

Does it affect the WordPress mobile app?

If the app logs in over XML-RPC with a password, it will stop working. Test with your own account first.

Can I remove it later?

Yes, by moving or renaming the file as described above.

Is blocking xmlrpc.php at the server better?

If nothing on your site needs XML-RPC, yes, because the requests never reach PHP. If you use Jetpack, it can break the connection, so use a filter like this one instead.

Sources

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *