WordPress XML-RPC: 3 Ways to Disable It & How to Identify xmlrpc.php Attacks | Jetpack Exception

Affiliate disclosure v3.1
Affiliate disclosure v3.2 (mobile)

If the access log of your WordPress website shows hundreds of xmlrpc.php requests in a row from the same IP, it is a password-guessing attack aimed at WordPress XML-RPC. I sorted one day's 8,624 access-log entries on a Nginx + Apache server running several WordPress sites, applied the three ways of disabling it myself and checked the response codes, to work out at which layer you have to block it before even PHP stops running.

XML-RPC is a protocol that exchanges XML documents over HTTP to call WordPress functions remotely. Since WordPress 3.5 there has been no on/off switch for it in the settings screen, so it runs enabled from the moment of installation.

1️⃣ What xmlrpc.php does and who uses it

Since the REST API arrived it is easy to think of [xmlrpc.php] as a file nobody uses any more, but Jetpack still connects your site to WordPress.com through it. Jetpack's official documentation states that it depends on the XML-RPC connection, and on hosts that block XML-RPC by default the Jetpack connection itself fails. It is like an office that still keeps one fax number around.

As of WordPress 7.1 this file accepts 77 methods: 50 in the wp family that handle posts, media and comments, 23 from the old blogging protocols (MetaWeblog, MovableType, Blogger), 2 pingback methods and 2 demo methods for testing.

The wp family logs in with the username and password carried in the request body, so it never passes through the login screen. Writing and editing posts, uploading media and managing comments all belong to this family, and the 23 old blogging-protocol methods are kept so that other blogging tools can exchange posts the same way.

demo.sayHello is a test call built into WordPress XML-RPC. Send it and it returns just one word, [Hello!]; since it needs no login, it is used to check whether XML-RPC is working. Its sibling demo.addTwoNumbers adds two numbers and returns the sum.

All of them respond only to an XML body sent by POST; open the URL in a browser and you get a 405 with the message [XML-RPC server accepts POST requests only.].

Pingbacks are this file's job too. When you publish a post, a pingback goes to the xmlrpc.php of every site you linked in the body, and the receiving server opens the sending URL itself to check that the link really exists.

🧭 Requests coming into xmlrpc.php and how they are handled
wp-admin SecurityExplainer diagram
📨 Sender
↓
Entrance
xmlrpc.php
Handles only an XML body sent by POST
405 · XML-RPC server accepts POST requests only.
77 methods
wp.*posts · media · comments, 50
metaWeblog · mt · bloggerold blogging protocols, 23
pingback.*pingbacks, 2
demo.*test calls to check it works, 2
Result
Keeps the connection to WordPress.com
Checks the username and password in the request body → writes and edits posts
The server opens the sending URL itself to check the link
No login needed: Hello! Response
No processing
Jetpack keeps its connection to WordPress.com through this file, so blocking the file breaks the connection.
External publishing tools log in with the username and password placed in the request body, without passing through the login screen.
A pingback is sent to the xmlrpc.php of the site linked in the post, and the receiving server opens the sending URL itself to check the link.
demo.sayHello is a test call that checks whether XML-RPC is working; it answers with the single word [Hello!] and needs no login.
Opening the URL in a browser returns a 405 with the message [XML-RPC server accepts POST requests only.].

2️⃣ xmlrpc.php attacks as seen in the access log

Password guessing does not go through the login screen; it comes in through [xmlrpc.php]. In the access log of a Nginx + Apache server running several WordPress sites, from midnight to 8 p.m. on October 14 there were 8,624 POST /xmlrpc.php requests from 72 IPs, and 19 of those IPs sent exactly 300 each.

One IP sent 300 requests in 28 minutes, from 14:09 to 14:37, at a steady 5–6 second interval to the end. 8,524 requests arrived with Jetpack or WordPress.com in the User-Agent, and the same IP rotated through several values imitating the real Jetpack format, such as [Jetpack by WordPress.com (Jetpack 13.0; WordPress 6.3)].

Until 2 p.m. that day the security plugin's XML-RPC off setting sent these requests back to the home page with a 302; after 2 p.m., once blocked in Nginx, they ended in 403. A 302 means PHP ran for every request even though the body was 0 bytes; a 403 is a response that never touched PHP.

🧭 An xmlrpc.php attack in one access-log line
Explainer diagram
🛡️ Server state
One access-log line (Nginx access.log)
203.0.113.25IP[14/Oct/2026:14:09:04 +0900]Time"POST /xmlrpc.php HTTP/1.1"Request302403Status0153Bytes"Jetpack by WordPress.com (Jetpack 13.0; WordPress 6.3)"User-Agent
PHP runs · 302 to homeNo PHP · Nginx default 403 page
With the security plugin's XML-RPC turned off, the log records a 302 that sends the request back to the home page. The body is 0 bytes, but PHP runs to produce that 302.
When blocked in Nginx, the log records a 403 and the 153-byte default error page, and PHP does not run.
Requests from the same IP
14:09:0414:37:20
300 in 28 minutes, 5–6 s apart
POST 8,624 from 00:00 to 20:0072 IPs19 IPs sent 300 each8,524 with a fake Jetpack/WordPress.com UA

3️⃣ system.multicall and password guessing

Many explanations say [system.multicall] lets one request test hundreds of passwords, but once a login fails inside a request, WordPress stops checking the passwords of the remaining calls. Only the first call goes through wp_authenticate; the rest are answered with the same error, [Incorrect username or password.], without being checked.

Because a response still comes back for every call, one request carrying 100 wrong passwords gets 100 errors in a 23 KB response within 0.12 seconds. If the correct password sits later in the same request, it is rejected along with the earlier failure; if it comes first, only that call succeeds. The password is compared once, but parsing the XML and loading WordPress repeat on every request, so the server load of this attack comes from the number of requests, not from the login checks.

4️⃣ 3 ways to disable XML-RPC and their response codes

Even with XML-RPC turned off in a plugin, [xmlrpc.php] requests still reach PHP. With the filter in place I sent a request carrying 100 calls and still got HTTP 200 with 100 errors (24 KB). The methods split into three according to the layer that rejects the request, and each has a different response code.

1. Turn it off in code with the xmlrpc_enabled filter

Add one add_filter line to your theme's functions.php that returns false for the [xmlrpc_enabled] filter. In this state, calls that need a login are refused with the error [XML-RPC services are disabled on this site.] and HTTP 405, but the test call demo.sayHello, which needs no login, still answers with 200.

2. Turn it off in plugin settings: Perfmatters · Hide My WP

This is the way of disabling it with a Disable XML-RPC setting in a plugin such as Perfmatters or Hide My WP. Hide My WP uses the same filter approach as above, so its responses are the same, but Perfmatters adds to the filter by ending direct [xmlrpc.php] requests with a 403 and removing the pingback header and pingback link, so once it is off both GET and POST answer 403. To produce that 403, however, WordPress still runs up to the init stage, so the response takes 0.13–0.15 seconds, the same as when it is open, and the PHP load remains.

🔌 Turning off XML-RPC in plugin settings
📍 Settings › Perfmatters › General › Core › Disable XML-RPC
Turn on the toggle and click [Save Changes]; the visitor's view below changes.
Admin screens
Settings saved.
perfmatters
⚙ GeneralVersion 2.6.8
Core
Disable Emojis?
Disable Dashicons?
Disable Embeds?
Disable XML-RPC?
Remove RSD Link?
Remove jQuery Migrate?
Hide WP Version?
Remove Shortlink?
Disable RSS Feeds?
Remove RSS Feed Links?
Disable Self Pingbacks?
Disable REST API?
Remove REST API Links?
Disable Google Maps?
Disable Password Meter?
Disable Comments?
Remove Comment URLs?
Add Blank Favicon?
Remove Global Styles?
Block Style Behavior?
Disable Heartbeat?
Heartbeat Frequency?
Limit Post Revisions?
Autosave Interval?
Login URL
Custom Login URL?
Disabled Behavior?
Message?
📍 Hide My WP › Settings › Cleanup › Disable XML RPC
Tick the box and click [Save Settings]; the visitor's view below changes.
Admin screens
Hide My WP
Settings was updated successfully!
Cleanup
Minify HTML
Remove comments and whitespaces from HTML (Use with cache).
Minify style
Remove theme info and other comments from stylesheet. (Require new style name)(Use with cache!).
Replace \/ URLs
Choose if you see Javascript URLs (e.g. \/wp-content\/themes)
Feed Meta
Other Meta
Default Tagline
Remove Version
Body Classes
Menu Classes
Post Classes
Clean up other
Replace in AJAX
Change Nonce
Disable XML RPC
Internal CSS
Useful to paste internal CSS code (Require New Theme Path)
Internal JS
Useful to paste internal JS code (Require New Theme Path)
Logged-out visitor's view — requests with the saved settings
405
XML-RPC server accepts POST requests only.
GET requests get a 405 under any setting.
200
Hello!
It is a test call to check operation, so it answers without login.
200
faultCode 403 · Incorrect username or password.
The password check runs. Changing the login URL makes no difference.
403
No body
Perfmatters ends it with a 403 at the init stage. PHP still runs.
403
No body
Perfmatters ends it with a 403 at the init stage. PHP still runs.
403
No body
Ends with a 403 without a password check. PHP still runs.
200
Hello!
It is the filter approach, so calls that need no login still answer.
405
faultCode 405 · XML-RPC services are disabled on this site.
Only calls that need a login are refused.

3. Block it at the web server: Nginx · Apache

Whether Nginx or Apache, the request is refused before PHP, so unlike the plugin methods WordPress never runs.

# Nginx — inside the server block, above the PHP location
location = /xmlrpc.php {
    deny all;
}

This location block goes above the PHP location, and Nginx refuses requests that match [xmlrpc.php] exactly on the spot. After adding it, test the configuration and reload for it to take effect.

nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
systemctl reload nginx

From the reload on, GET and POST end in 403 without reaching PHP; the response body is the 153-byte Nginx default 403 page and the response takes 0.02 seconds.

# Apache — .htaccess in the site root (Apache 2.4)
<Files xmlrpc.php>
    Require all denied
</Files>

On a server that runs only Apache, put the Files directive above into the .htaccess in the site root and it applies immediately; the Apache default 403 page (199 bytes) comes back in the same 0.02 seconds.

curl -s -o /dev/null -w '%{http_code}' -X POST https://example.com/xmlrpc.php -d '<methodCall><methodName>demo.sayHello</methodName></methodCall>'
403

The command above sends the test call demo.sayHello by POST and checks the response code; after blocking at the web server it returns 403. Leave out the [-o /dev/null] and [-w] options and you see the body: [Hello!] means it is open, and a site turned off by the filter alone gives the same answer, so you have to send a login call as well to confirm the 405.

5️⃣ When you should not turn it off, and the alternatives

Blocking it entirely at the web server has the biggest effect, but it does not suit sites connected to WordPress.com through Jetpack. Jetpack depends on the XML-RPC connection, and on hosts that block XML-RPC by default, such as WP Engine, you have to open an exception following Jetpack's guidance before it connects. The same goes for sites that want to receive pingbacks.

Such sites narrow it down by allowing only the IP list Jetpack publishes (jetpack.com/ips-v4.txt) and denying the rest. Inside the Nginx location block, add one allow line per IP range and finish with deny all.

If you cannot block it completely, what remains is a per-minute limit on requests that are not logged in. The setup is the same as the Nginx rate-limit section in the post wp-admin Security , and since it counts only requests without a login cookie, it does not affect logged-in admin work.

🔢 FAQ & Recommended Content

Even if you delete it, the next WordPress core update copies the file from the package back into place, so it is restored at the next update. Blocking that URL at the web server or disabling it with the filter stays in effect after updates, unlike deleting the file.

The REST API is a separate entrance that comes in through the [wp-json] URL, so it stays open even with XML-RPC disabled. The block editor uses this entrance, so the right approach is not to disable it completely but to block only logged-out requests.

They do not check; they just send. Every WordPress site has this file at the same path, so scanners simply change the domain and send POST requests. A default WordPress install even exposes the xmlrpc.php URL itself, through the [EditURI] link in the page head and the [X-Pingback] header on posts that accept pingbacks.

🛡️ WordPress security: recommended content 8 posts · 2 external
🗂️ WordPress security
📋 wp-admin security 📄 Brute force: 9 defenses 📄 Hide My WP 📄 Security plugins: Best 5
📄 Disable XML-RPC 📄 REST API 📄 Pingbacks · trackbacks 📄 wp-config.php security
📡 Nanaco Japan: dealing with xmlrpc.php attacks 📡 サイトの主治医: disabling xmlrpc.php and checking it
No search results.

ℹ️ Affiliate Disclosure
This site's content contains affiliate links. When a visitor buys a product or service through one of them, the site receives a commission from the seller. The amount the buyer pays(it goes down during event discounts ↓)does not go up. Posted prices, discounts, and stock reflect the time of writing and may differ, so confirm with the seller before buying. Products are chosen and reviewed by our own standards, and commissions do not affect the order or content of reviews.