Security

Securing the WordPress REST API; Closing the Path to User Leaks

If your WordPress endpoint is leaking the user list or bots are hammering it with login attempts, this guide closes exactly the spots that are truly dangerous.

Security

A single request is enough: curl -s https://example.com/wp-json/wp/v2/users. If the JSON output was full of slug and name, then right now the usernames of all your site's authors and administrators are public. The attacker doesn't need to guess; they grab the list and then just try passwords against those real names. This is the starting point of most credential stuffing attacks on WordPress.

The first reaction of many is to shut down the entire REST API. This works, but you pay the cost later: the Gutenberg block editor uses that same API to save posts, and so do form and store plugins. The site comes up looking healthy and then throws a vague error when saving a draft or placing an order. So the goal is to close the sensitive endpoints, not to kill the whole service.

Why the WordPress REST API leaks the user list

This behavior isn't a bug; it's the default design. The /wp-json/wp/v2/users route was built for creating client-side interfaces and by default only returns users who have at least one published post. The problem is that on many sites the administrator is also an author, so the administrator's username ends up in that same list.

The clean solution is to filter this endpoint for guest users. Put this code in your child theme's functions.php or an mu-plugin:

add_filter( 'rest_endpoints', function( $endpoints ) {
    if ( ! is_user_logged_in() ) {
        unset( $endpoints['/wp/v2/users'] );
        unset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] );
    }
    return $endpoints;
} );

After this change, that first curl should return {"code":"rest_no_route","message":"No route was found..."}. If you still see the list, it means the page cache or object cache is still serving the old version; clear the cache and test again.

This is where they go wrong

The common mistake is closing only the /wp/v2/users route and forgetting the ?author=1 route. With https://example.com/?author=1, WordPress redirects you to the author archive, and in the final URL, author_name is exposed. The sign is that your REST API test passes but external scanning tools still find the username. To close this path, disable the author archive redirect or send it to the homepage with a rewrite rule.

Restricting sensitive endpoints and rate limiting

Closing the user list isn't enough. Two other endpoints must also be controlled: /wp/v2/posts, which can leak draft content under some settings, and the authentication route, which doesn't exist in WordPress core but is added by JWT plugins and applications.

For rate limiting, you have two paths. If you have server access, put this inside the server block in Nginx:

limit_req_zone $binary_remote_addr zone=wpapi:10m rate=20r/m;

location /wp-json/ {
    limit_req zone=wpapi burst=5 nodelay;
    limit_req_status 429;
    try_files $uri $uri/ /index.php?$args;
}

The number 20r/m means twenty requests per minute from each IP. For ordinary sites this number is generous; but if your site has a mobile app or makes heavy use of the block editor page, this same number will cause 429 errors for real users. In that case, raise the burst or exempt the /wp-json/wp/v2/ route from the limit and only be strict on the authentication routes.

If you don't have access to the server config, security plugins do the same thing at the PHP layer, but note that every request still reaches PHP and you pay the processing cost. For high-traffic sites, rate limiting at the network edge is better than rate limiting in PHP. If your site is also a target of volumetric attacks, combining these settings with DDoS protection is more sensible than relying on a single layer.

Authentication on the WordPress REST API

By default, WordPress uses cookies and nonces for REST authentication. This is fine for the browser, but it doesn't work for external applications, and this is what pushes developers toward Application Passwords or JWT.

Enable Application Passwords from Users → Profile. Each password is tied to a specific device and can be revoked at any moment. I prefer this over JWT, because JWT in WordPress implementations usually has a shared secret key, and if it leaks, there's no way to invalidate issued tokens until they expire. An Application Password can be deleted that very moment.

A point that's rarely observed: each Application Password should be for only one service. If you share one password between a backup script and a store plugin, when a leak happens you won't know which path was compromised and you'll have to revoke everything. For managing these passwords in a team, the same principles described in password management in teams apply here exactly.

Security headers that actually prevent abuse

Three headers have the most effect on REST responses:

  • X-Content-Type-Options: nosniff so the browser doesn't interpret the JSON response as HTML.
  • A restrictive Content-Security-Policy, so if XSS executes somewhere, it can't send a REST request with the user's cookie.
  • A specific Access-Control-Allow-Origin instead of *, so other sites can't send requests from your user's browser.

An open CORS header isn't a vulnerability on its own, but when combined with an authentication cookie, it becomes a full CSRF. If a plugin has opened this header, prioritize that one.

Monitoring and logging REST requests

Without logs, you won't know someone is scanning your endpoints. The least you can do is log 401 and 429 requests with IP and path. In Nginx:

log_format apilog '$remote_addr $status $request_uri $http_user_agent';
access_log /var/log/nginx/wpapi.log apilog;

Then with a quick grep " 429 " /var/log/nginx/wpapi.log | awk '{print $1}' | sort | uniq -c | sort -rn | head you'll see the fastest attackers. If one IP has gotten hundreds of 429s in a few minutes, it's time to block it in the firewall. Keep these logs separate from the main access log and make sure they have rotation, otherwise they'll fill up the server disk. The principles of retaining and keeping these logs intact are the same as what's explained in what an audit log is.

Which option should I choose?

ApproachAdvantageCostWhere I choose it
Fully disabling the REST APISmaller attack surfaceBreaks Gutenberg and pluginsStatic sites without a block editor
Filtering endpointsSecurity without breaking the siteRequires code maintenanceMy default choice for most sites
Rate limiting in NginxNo load on PHPRequires server accessHigh-traffic sites and attack targets

If you can only do one thing, filter /wp/v2/users and close ?author=. These two have the highest return with the lowest risk of breaking the site. Add rate limiting after that, once you're sure it won't block the site's normal traffic.

Before any change, take a backup of the database and files, and after applying, test with curl and a real browser. If your site is hosted on ServerNet infrastructure and you want to reinforce these layers from the network side as well, security services is a sensible starting point. For a quick check of DNS records and network paths, DNS and network lookup tools make the job easier.

Frequently asked questions

Is fully disabling the WordPress REST API more secure?

Not necessarily. Full disabling reduces the attack surface, but the block editor, form plugins, and many store plugins use that same API and will stop working. In most cases, filtering sensitive endpoints like the user list gives a better result.

How do I find out if my WordPress REST API is leaking the user list?

Send a simple request to https://example.com/wp-json/wp/v2/users. If the JSON response includes users' names and slugs, your list is public. Test the ?author=1 route separately too, because one may be closed while the other is open.

What number should rate limiting on the REST API be?

For ordinary sites, twenty requests per minute from each IP is a good starting point. If you have a mobile app or many users with the block editor, this number will cause 429 errors for real users and you should raise the burst or exempt read routes.

Is Application Password more secure or JWT?

For most sites I prefer Application Password, because each password is tied to a device and can be revoked at any moment. JWT in WordPress implementations usually has a shared key and invalidating issued tokens in it isn't easy.

ServerNet Support

ServerNet engineering & editorial team — specialists in infrastructure, networking and web hosting.

Security Services
Share:

Comments 0

No comments yet — be the first!

Leave a comment

Related service

Security Services

Penetration testing by OSCP-certified specialists, infrastructure hardening and 24/7 security monitoring — reports managers understand and engineers can act on.