The fix, in one snippet
add_filter('rest_endpoints', function ($e) {
unset($e['/wp/v2/users'], $e['/wp/v2/users/(?P<id>[\d]+)']);
return $e;
});
Illustrative values. Change the paths, hostnames and options to match your own site before using it.
The sections below explain what is tested, why it matters and the alternatives.
What we test
Scan.now sends GET /wp-json/wp/v2/users. A finding requires a 200 response containing a JSON array with slug fields, which are the users' user_nicename values and by default equal their login names. It also requests /?author=1 and treats a redirect to /author/<name>/ as a second enumeration route. The report shows how many accounts were listed and the first few slugs, so you can see whether they match real logins. A 401 response from the REST route, or an author query that returns 404 or the homepage, passes. Scan.now stops at enumeration: it never attempts a login. Rated medium, and higher in practice when xmlrpc.php is also enabled, because the two together make credential guessing efficient. This check runs only when WordPress is detected.
Why it matters
A password guess against an unknown username is two unknowns; against a known one it is a dictionary attack. WordPress's REST API lists every user who has published a post, and the author-archive redirect resolves numeric IDs to names, so an attacker collects the administrator's login name in one request and feeds it to xmlrpc.php or wp-login.php. The project considers usernames public by design (they appear on author pages) and does not treat this as a vulnerability, which is a reasonable position for a blog with strong passwords and two-factor authentication and a poor one for a site whose admin password is a pet's name. There is a privacy angle too: the list reveals staff and contributors, useful for social engineering. Reducing exposure costs nothing and removes the easiest reconnaissance step. The WordPress scanning guide places this in the hardening order.
How to fix it
Hide the user routes from anonymous requests while keeping them for logged-in editors (the block editor needs them):
add_filter('rest_endpoints', function ($endpoints) {
if (!is_user_logged_in()) {
unset($endpoints['/wp/v2/users'], $endpoints['/wp/v2/users/(?P<id>[\d]+)']);
}
return $endpoints;
});
// stop ?author=N resolving to /author/name/
add_action('template_redirect', function () {
if (is_author() && isset($_GET['author'])) { wp_redirect(home_url(), 301); exit; }
});
nginx can refuse the numeric author query before it reaches PHP:
if ($args ~* "^author=\d+") { return 403; }
Apache (mod_rewrite, in .htaccess):
RewriteCond %{QUERY_STRING} ^author=\d+ [NC]
RewriteRule ^ - [F]
Cloudflare WAF custom rule: block /wp-json/wp/v2/users for requests without a wordpress_logged_in cookie. Set each user's public display name to something other than their login, and make the login itself defensible: long unique passwords, two-factor authentication, and a login limiter such as the one built into most security plugins.
Where this fits
WordPress user enumeration via REST API is check 7 of 14 that the website vulnerability scanner runs under exposed files and information disclosure, ordered the way they are worth fixing. That ordering is the point: Fixing this one while the check above it still fails buys less than it looks like.
Fix this one first
Above it in the same category sits Exposed server status page (medium), where a server status page (Apache mod_status, nginx stub_status or PHP-FPM status) is reachable from the internet, revealing live requests, client IPs, internal hostnames and load details. An attacker who has that does not need this, so it is the better use of the same hour.
What fixing this still leaves open
WordPress user enumeration via REST API closes one route in. Immediately below it: WordPress xmlrpc.php enabled, where the WordPress XML-RPC endpoint at /xmlrpc.php is enabled; Exposed .DS_Store file, where a macOS .DS_Store file is served from the web root; HTTP TRACE method enabled, where the server answers HTTP TRACE requests by echoing them back.
Found in the same scan
The website vulnerability scanner reports this alongside checks from other categories that are at least as serious, including Certificate chain and hostname validation, where the certificate presented for this hostname did not validate: The chain does not reach a trusted root, an intermediate is missing, the name does not match, or the certificate is self-signed or expired, and HTTPS is available, where the site could not be reached over HTTPS on port 443, or the TLS handshake failed. A single run of website vulnerability scanner answers all of them at once.
The first few lines
You are a senior web engineer. I ran a security and SEO scanner against my site and it reported the finding below. Fix it properly rather than suppressing the symptom.
Finding: WordPress user enumeration via REST API (medium severity)
Scanner check id: wordpress-user-enumeration
The rest of this prompt names the pages and line numbers we found the problem on, the configuration to change, and the constraints a good answer has to respect. It is free, it just needs an account so the work is not scraped wholesale.
Sign in with GoogleSigning in is free and takes one click. We store your email address and nothing else.