WordPress User Enumeration
Updated August 24, 2026
4 min read
A default WordPress install hands valid login names to anonymous visitors through the REST API, ?author=N redirects and the core user sitemap. That gives an attacker the exact string to type into the wp-login.php username field — half the credential pair, for free. Rated low: it is reconnaissance, not a break-in, and it converts to access only against a weak or reused password. The fix is configuration only — a must-use plugin plus login rate limiting, under an hour of work.
What is WordPress user enumeration?
It is the WordPress-specific case of username enumeration, which covers the generic oracle and defence. WordPress earns its own page because the login name doubles as the public author identity, and four core surfaces publish it by default:
/wp-json/wp/v2/users— returns JSON for every user with a published post in ashow_in_restpost type:id,nameandslug.slugisuser_nicename, which defaults to the login name. No auth needed, and blocking the path alone fails —/?rest_route=/wp/v2/usershits the same handler.?author=N— core'sredirect_canonical()301s any ID with a published post to/author/<slug>/, leaking the slug in theLocationheader. IDs without published posts do not redirect, so the response sorts real authors from the rest./wp-sitemap-users-1.xml— core sitemaps (WP 5.5+) enumerate author archives for you.wp-login.php— core answers "The username X is not registered on this site" for a missing account and "The password you entered for the username X is incorrect" for a real one.
That is A02:2025 Security Misconfiguration bleeding into A07:2025 Authentication Failures — see the OWASP Top 10 breakdown. Like directory listing, it is information the server never had to hand out.
Why it's a risk / how it's exploited
One command does it. wpscan --url https://example.com --enumerate u walks the REST route, the author IDs and the sitemap and prints admin, marketing, jsmith — IDs 1–10 by default, --enumerate u1-100 goes wider. Those names then feed a password spray against wp-login.php or xmlrpc.php, which login-page CAPTCHAs and many rate-limit rules never cover.
On xmlrpc, be accurate: the classic system.multicall amplification trick — hundreds of wp.getUsersBlogs guesses per request — died in WordPress 4.4, which fails every remaining authenticated call after the first failure. xmlrpc.php is still a convenient brute-force endpoint at one guess per request, but its generic "Incorrect username or password" makes it no enumeration oracle.
Preconditions, plainly: enumeration alone compromises nothing. It converts to access only if one of those accounts has a weak, reused or breach-corpus password and no second factor — which is why this sits at low while the parent entry rates the generic login oracle medium. Its other use is targeting: a known editor name makes a phishing lure far more convincing.
How to detect it
# 1. Anonymous REST user listing, plus the path-block bypass
curl -s https://example.com/wp-json/wp/v2/users | head -c 300
curl -s "https://example.com/?rest_route=/wp/v2/users" | head -c 300
# 2. Author ID -> slug redirect (do not follow; read the Location header)
curl -sI "https://example.com/?author=1" | grep -i '^location'
# 3. Core user sitemap (WordPress 5.5+)
curl -s https://example.com/wp-sitemap-users-1.xml | grep -o '<loc>[^<]*</loc>'
# 4. Login oracle: two different core error strings
curl -s -d "log=nosuchuser0x&pwd=x" https://example.com/wp-login.php | grep -o 'is not registered on this site'
curl -s -d "log=admin&pwd=x" https://example.com/wp-login.php | grep -o 'password you entered for the username'
Vulnerable looks like 200 OK with [{"id":1,"name":"Admin","slug":"admin",…}] and a 301 carrying Location: https://example.com/author/admin/. Hardened looks like 404 with {"code":"rest_no_route"} once the route is unregistered (a gating plugin returns 401 or 403 instead), plus an ?author=1 request that lands on the homepage. Fold these into your routine scanning workflow.
How to fix it
Drop this in wp-content/mu-plugins/no-user-enum.php so no update can revert it:
<?php
add_filter( 'rest_endpoints', function ( $endpoints ) {
if ( is_user_logged_in() ) { return $endpoints; }
unset( $endpoints['/wp/v2/users'] );
unset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] );
return $endpoints;
} );
// Priority 0 matters: redirect_canonical() is hooked to template_redirect at 10
// and fires the /author/<slug>/ redirect before a default-priority callback runs.
add_action( 'template_redirect', function () {
if ( isset( $_GET['author'] ) ) {
wp_safe_redirect( home_url(), 301 );
exit;
}
}, 0 );
add_filter( 'wp_sitemaps_add_provider', function ( $provider, $name ) {
return 'users' === $name ? false : $provider;
}, 10, 2 );
/wp/v2/users/me stays registered on purpose: it needs a session, and the block editor uses it. Next, split the public slug from the login name with WP-CLI — wp user update jsmith --user_nicename=j-smith --display_name="J. Smith" — then throttle the login endpoint at the edge:
limit_req_zone $binary_remote_addr zone=wplogin:10m rate=1r/s; # http{} block
# server{} block
if ($arg_author) { return 403; }
location = /xmlrpc.php { deny all; } # only if nothing uses it
location = /wp-login.php {
limit_req zone=wplogin burst=5 nodelay;
include snippets/fastcgi-php.conf; # Debian/Ubuntu path
fastcgi_pass unix:/run/php/php8.2-fpm.sock; # match your PHP version
}
On Apache the author block needs mod_rewrite:
RewriteEngine On
RewriteCond %{QUERY_STRING} (^|&)author=\d
RewriteRule ^ - [F,L]
Caveat: this is hardening, not an authentication fix. Names still leak from bylines, comment authors and <dc:creator> in /feed/, and any logged-in subscriber can query the REST route again. Blocking ?author= breaks legitimate author archives, changing user_nicename changes existing /author/<slug>/ URLs, and anonymous headless front ends break outright. Unique passwords plus 2FA are what stop the spray.
FAQ
Is WordPress user enumeration really a vulnerability?
Core treats the REST users route as intended behaviour, so it is information disclosure, not a flaw, and we rate it low like any other recon leak. On a site with unique passwords and 2FA it is noise. On a site with an admin account, no lockout and an open xmlrpc.php, it is a real step in a working chain.
Does hiding usernames stop brute-force attacks?
No — it raises cost. Bots try admin, administrator and the domain name regardless of what you expose, so this only removes the shortcut to your real accounts. Rate limiting, a lockout plugin and 2FA for every editor-and-above role are the controls that end the attack.
Exploita's AI pentest agent checks all four enumeration surfaces on every WordPress target and reports only what it can prove — no "possible user disclosure" guesses. Run a free website vulnerability scan to see which usernames your site is handing out right now.
