A request to https://example.com/?author=1 on WordPress triggers a 301 redirect to the corresponding author archive — and the destination URL exposes user_nicename, which is usually identical to user_login. An attacker can script ?author=1 through ?author=20 and within seconds gather a list of every administrator and editor on the site. This is "username enumeration" — the twin of REST API user disclosure.
Why this matters
A brute-force attack has two phases: discovering a valid username and guessing the password. Username discovery alone narrows the search space by orders of magnitude. If an attacker knows that geffen is an admin (because ?author=1 revealed it), they no longer need to try admin, administrator, root, or fifty other generic guesses — they go straight to the right login. Worse, ID 1 is almost always the original super admin, and many people reuse passwords across sites — the attacker can check that user against known credential leaks (Have I Been Pwned) before any brute force ever begins.
How to detect
In a private window, visit https://example.com/?author=1. If the browser ends up on a URL like https://example.com/author/geffen/, the leak is active. Test ?author=2, ?author=3, and so on. The fullest detection tool is WPScan: wpscan --url https://example.com --enumerate u — it lists every user it could enumerate via this vector.
How to fix
The cleanest approach is to reject any request with an ?author= parameter at the WordPress layer, before the redirect runs. Add to an mu-plugin or child theme:
add_action('init', function () {
if (is_admin()) return;
if (!empty($_GET['author']) || !empty($_REQUEST['author'])) {
wp_die('Forbidden', 'Access Denied', ['response' => 403]);
}
}, 1);
// Also short-circuit canonical redirects that would expose the slug
add_filter('redirect_canonical', function ($redirect, $request) {
if (preg_match('/\?author=\d+/', $request)) {
return false;
}
return $redirect;
}, 10, 2);
At the web server layer:
RewriteEngine On
RewriteCond %{QUERY_STRING} (^|&)author=\d+ [NC]
RewriteRule ^ - [F,L]
if ($arg_author) {
return 403;
}
Defense in depth: change user_nicename so it differs from user_login:
UPDATE wp_users SET user_nicename = CONCAT('user-', SUBSTRING(MD5(RAND()), 1, 8)) WHERE ID IN (1,2,3);
Common mistakes
Mistake one: blocking only /?author=N via .htaccess without addressing /author/SLUG/. Even with the redirect blocked, an attacker who guesses the slug can still hit the author page directly. If you do not use author archives at all, disable them entirely: add_action('template_redirect', function(){ if (is_author()) wp_redirect(home_url(), 301); });. Mistake two: assuming "I renamed admin to geffen, I am safe". That does not help — enumeration returns geffen just as easily. The only real fix is to block the vector. Mistake three: ignoring the REST API path. Both user_enum and rest_users_public need to be addressed together.
Verifying the fix
In a private window, hit ?author=1 through ?author=10. Each must return 403 or 404 — never a redirect to an author page. Re-run wpscan --url https://example.com --enumerate u — the output should read "No usernames found". Finish with the audit and confirm both user_enum and rest_users_public pass.
?author=N parameter and keep /author/SLUG/ reachable. But make sure every slug is distinct from any login.