You added the filter. One line in functions.php, copied from the top search result: add_filter( 'xmlrpc_enabled', '__return_false' );. Then a week later you opened the access log and xmlrpc.php was still there, still answering, still collecting a few hundred POST requests a day from hosts you’ve never heard of. The filter didn’t fail. It did precisely what it’s documented to do. The problem is that its name describes something it was never built to accomplish, and WordPress core says so in its own source code.
XML-RPC is the remote-publishing interface WordPress shipped long before the REST API existed. It lets an external client authenticate over HTTP and create posts, upload media, edit comments, and read your settings, all by POSTing a lump of XML at a single file in your web root. It also carries pingbacks, which is the feature that turns your server into something an attacker can point at other people. Since WordPress 3.5 it’s been on by default with no setting in the admin to change that, which means every install you’ve ever spun up has been quietly running it.
This guide covers what the interface actually exposes, why the most-recommended fix only closes part of the door, which two methods matter from a security standpoint (including one widely repeated claim that’s out of date on current core), and three levels of shutting it down depending on how much you’re willing to break.
What XML-RPC Is, and Why It’s Still in Your Install
Open xmlrpc.php in your WordPress root and it’s barely a hundred lines. It defines a constant, reads the raw request body, loads WordPress, and hands everything to wp_xmlrpc_server. That class is where the actual surface lives, and it’s large: methods namespaced under wp., metaWeblog., blogger., and mt., covering posts, pages, media, taxonomies, comments, users, and site options. There are two pingback methods. There are even a couple of leftover demo methods, demo.sayHello and demo.addTwoNumbers, which do exactly what they sound like.
One detail in that file explains the security shape of the whole thing. Near the top, before WordPress loads, it runs $_COOKIE = array(); with a comment about discarding cookies sent by browser-embedded clients. So XML-RPC doesn’t use your login session at all. Every authenticated call carries its credentials inside the XML body, on every single request. That’s a design decision from an era of desktop blogging clients, and it’s the reason the endpoint is such a comfortable target: it’s a login form that accepts unlimited structured input and doesn’t care about cookies, nonces, or anything your admin-side hardening does.
Why 3.5 matters more than you’d think
Before WordPress 3.5, XML-RPC was opt-in behind a checkbox on the Writing Settings screen. The 3.5 release notes list two relevant changes: “Enable XML-RPC by default, remove Writing Settings option,” and separately, “XML-RPC: Now always enabled and supports fetching users, managing post revisions, searching.” Both the on-switch and the admin control disappeared in the same release, and the capability set grew at the same time.
That’s why there’s no toggle to hunt for. If you want it off, you’re writing code, installing a plugin, or configuring your web server. And it’s why so many “disable XML-RPC” tutorials exist, most of them recommending the same one-liner that doesn’t do the whole job.
The Filter Everyone Recommends Is Half a Fix
Core’s own docblock for the xmlrpc_enabled filter is unusually blunt about this. It reads, verbatim: “Contrary to the way it’s named, this filter does not control whether XML-RPC is fully enabled, rather, it only controls whether XML-RPC methods requiring authentication – such as for publishing purposes – are enabled.” It goes on: “Further, the filter does not control whether pingbacks or other custom endpoints that don’t require authentication are enabled.”
The docblock even explains why. The filter was introduced in 3.5 to replace the old enable_xmlrpc UI option, and it was scoped to match that option’s behaviour rather than to be a master switch. That’s a backwards-compatibility decision from thirteen years ago that’s still shaping what a one-line fix does today.
What the filter actually turns off
Returning false from xmlrpc_enabled makes the server’s login() method bail immediately with XML-RPC fault code 405 and the message “XML-RPC services are disabled on this site.” That’s a fault code inside the response body, not an HTTP status. Anything that needs credentials stops working: publishing from a desktop client, the WordPress mobile app’s posting features, most Jetpack functionality on older connection methods, and any integration that authenticates over XML-RPC. That’s real protection, and if credential stuffing against the endpoint is your worry, this closes it.
What keeps answering
Everything that never needed a login in the first place. pingback.ping doesn’t authenticate, so it stays live. So do the introspection methods that come from the underlying IXR server library, system.listMethods, system.getCapabilities, and system.multicall. The file itself still parses XML and still consumes PHP workers and database connections on every request. If your reason for disabling XML-RPC was that a botnet is hammering it and driving your load up, the filter alone won’t change your traffic graph much.
A fix you believe in is worse than no fix at all, because you stop looking. Plenty of sites are running that one-line filter, showing a green tick on a security checklist, and still serving unauthenticated pingback requests to anyone who asks. The filter is a genuine control with a genuinely misleading name, and core documents the gap in the source. Knowing exactly which half it covers is the difference between hardening your site and decorating it.
The Two Methods Attackers Actually Want
Out of everything the server exposes, two methods account for most of the abuse traffic you’ll see in a log. They get abused for completely different reasons, and only one of them still works the way the internet says it does.
system.multicall, and the brute-force story that’s out of date
The claim you’ll read everywhere is this. system.multicall batches many method calls into a single HTTP request. Wrap a few hundred wp.getUsersBlogs calls in one multicall, each with a different password guess, and you get hundreds of login attempts for one request, sailing past any rate limiter that counts requests rather than attempts. It was a real technique and it worked well for years.
On current core it doesn’t. Look at how wp_xmlrpc_server handles a failed login: it sets a $auth_failed property on the server instance, and every subsequent login() call on that same instance short-circuits to a login_prevented error without ever calling wp_authenticate(). Because a multicall is handled by one server instance inside one request, the first wrong password poisons the rest of the batch. Your whole batch of guesses tests exactly one password.
Core also refuses to nest them, returning “Recursive calls to system.multicall are forbidden” for a multicall inside a multicall, and there’s a separate xmlrpc_element_limit filter, defaulting to 30,000, that makes the parser bail on absurdly large documents before it does any work. It applies to every XML-RPC request rather than just batched ones, and it counts opening angle brackets as a rough proxy for elements, refusing anything past twice the limit. I’d still block the endpoint, and I’d be careful about how much comfort to take from this. What core killed is the amplification, not the brute force. There’s no lockout and no throttling here, so an attacker still gets one free, unlogged-by-WordPress guess per request against an endpoint that never sleeps. It’s the difference between hundreds of guesses per request and one, which is a real improvement and not a solved problem.
pingback.ping, and why your server makes the request
Pingbacks work by having one site tell another that it linked to it. To verify the claim isn’t fabricated, your server fetches the source URL and checks that the page really contains a link back to you. That verification step is the whole problem: an unauthenticated stranger can make your server issue an outbound HTTP request to an address they choose.
Point thousands of WordPress sites at one victim simultaneously and you’ve built a reflective denial-of-service amplifier out of other people’s servers. This was widely abused, and it’s the main reason “disable XML-RPC” became standard hardening advice.
Core has tightened the verification fetch considerably since then. It runs through wp_safe_remote_get(), which applies WordPress’s protections against requests to internal addresses. It sets a 10-second timeout, refuses to follow redirects at all, and caps the downloaded response at 150 KB so a malicious source URL can’t feed your server an enormous file. It also forwards the requesting IP in an X-Pingback-Forwarded-For header, so a target that’s being reflected at can at least see where the pings originated. WordPress 7.1 added the wp_auto_approve_ping filter. Core’s default decision there is to auto-approve a pingback only when it came from a published post on your own site, holding everything else for moderation, and the filter lets you override that.
Those are meaningful mitigations. They don’t change the fundamental shape, though, which is that an anonymous request still causes your server to do work on someone else’s behalf. If you don’t use pingbacks, and almost nobody does anymore, there’s no reason to keep offering that.
A site we were called into last year had gone slow rather than obviously broken, which is the harder kind to diagnose. The access log explained it in about a minute: page views were unremarkable, and roughly 40% of all requests were POSTs to one file in the web root. Nobody had noticed because the site never went down, it just felt bad, and the host had been recommending a bigger plan. Blocking the endpoint at the server took ten minutes and the load average halved. The lesson we took away wasn’t about that one file, it was that “the site feels slow” is worth grepping an access log over before it’s worth buying more CPU over.
How to Actually Shut It Down: Three Levels
Pick based on what you still need working. These aren’t a ladder. Levels 1 and 2 close different surfaces, the unauthenticated one and the credentialed one, so they’re meant to be stacked rather than chosen between. Level 3 covers both on a given server, and the filters are still worth keeping underneath it in case you ever move hosts.
Level 1: Strip the methods you don’t want
The xmlrpc_methods filter has been in core since 1.5 and gives you WordPress’s method table as a plain array before the server dispatches anything. Remove the keys you don’t want and those methods stop existing. This is the surgical option, and it’s the right one if you genuinely use a client that publishes over XML-RPC but want the pingback surface gone.
add_filter( 'xmlrpc_methods', function ( $methods ) {
unset( $methods['pingback.ping'] );
unset( $methods['pingback.extensions.getPingbacks'] );
return $methods;
} );
One limit worth knowing before you reach for this: the system. introspection methods aren’t in that table. They’re added by the IXR server class afterwards, so unset() on system.multicall looks like it works and quietly does nothing. Those three are a Level 3 job.
Level 2: Turn off the authenticated methods too
Add the filter everyone recommends, now that you know what it covers. Combined with Level 1 you’ve closed the credentialed surface and the pingback surface, which between them account for the reasons most people want this off.
add_filter( 'xmlrpc_enabled', '__return_false' );
Put both of these in a small site-specific plugin rather than in your theme’s functions.php. A theme switch would leave your hardening behind, and this is exactly the kind of setting you don’t want silently reverting the day someone tries a new design.
Level 3: Block the file at the web server
This is the one that actually changes your traffic graph. The two filters above still let the request reach PHP, boot WordPress, and connect to your database before anything gets refused. Denying the file at nginx or Apache means the request costs you a few bytes and a 403, with no PHP process involved.
# nginx
location = /xmlrpc.php {
deny all;
# Leave access_log on for the first week so you can see
# what you are blocking, then switch it off if the volume is noisy.
# access_log off;
}
# Apache
<Files "xmlrpc.php">
Require all denied
</Files>
Don’t delete the file. It’ll come back on the next core update, and a modified core install makes every future integrity check noisier than it needs to be. If your host offers an XML-RPC toggle in its control panel, that’s usually this same server-level rule with a nicer interface, and it’s the least breakable place to put it.
Clean up the advertising while you’re in there
WordPress announces the endpoint in two places, and both are worth removing once it’s blocked. rsd_link() is hooked to wp_head and prints a <link rel="EditURI"> tag pointing at xmlrpc.php?rsd. Separately, WordPress sends an X-Pingback response header, though only on single posts where pings are open, so a site with comments and pings closed won’t be emitting it at all.
remove_action( 'wp_head', 'rsd_link' );
add_filter( 'wp_headers', function ( $headers ) {
unset( $headers['X-Pingback'] );
return $headers;
} );
Removing these is tidiness rather than security. Anyone scanning for the endpoint will simply request the file directly; they aren’t reading your head tags for permission. It does stop legitimate clients from auto-discovering an interface you’ve turned off, which saves confusing failures later.
Three quick tests that tell you where you actually stand:
- Visit
https://yoursite.com/xmlrpc.phpin a browser. A blocked site returns 403 or 404. If you see the message about only accepting POST requests, the endpoint is live. - View source on any post and search for
EditURI. If the tag is there, WordPress is still advertising the interface. - Grep your access log: count how many requests hit
xmlrpc.phpin the last day, and compare that to your real pageviews. A wide gap is the argument for blocking at the server rather than in PHP.
Check What Breaks Before You Block It
Turning this off is safe on most sites, but “most” isn’t “all,” and the failures are quiet ones that show up days later. Things worth confirming first: whether Jetpack is connected through the older XML-RPC method rather than a modern connection, whether anyone posts from the WordPress mobile app or a desktop editor, whether a CRM, e-commerce sync, or backup service authenticates that way, and whether you actually want incoming pingbacks from other sites.
The honest answer for most small business sites is no to every one of those. Modern integrations use the REST API, and if something needs credentialed programmatic access, Application Passwords give you a revocable, per-application secret instead of your real login. Worth clearing up a common misconception here: Application Passwords do work over XML-RPC, because core defines an XMLRPC_REQUEST constant specifically so they’re accepted there. So the credential model isn’t really the argument for switching. The argument is surface area, and how much of it you’re leaving exposed for a client you probably retired years ago.
If you’re unsure, block at the server for a week and watch your error log and your inbox. Anything that depended on the endpoint will surface fast, and reversing a few lines of server config takes a minute.
A Sensible Default for Most Small Sites
Block xmlrpc.php at the web server, add the two filters as a backstop in case you migrate to a host where the server config doesn’t follow, and remove the RSD tag so nothing goes hunting for an interface that isn’t answering. If your host has a toggle for it, use theirs and skip the config editing.
Then move on, because this is one item on a list. The endpoint is a real surface with a genuinely confusing off-switch, and it deserves ten minutes. It isn’t the thing that gets most sites compromised. Weak admin passwords, abandoned plugins, and unpatched core are still ahead of it, and none of them are fixed by anything in this article.
Frequently Asked Questions
Not sure what your install is currently exposing? Run the three checks above. If the endpoint answers and your log is full of POSTs you can’t account for, that’s a ten-minute fix sitting in front of you.
Built by amplifi.studio — see also our WordPress security hardening checklist.