User Enumeration via ?author=N: Username Discovery

Visiting /?author=1 triggers a redirect that exposes the login slug. Block it without breaking author archives.

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.

Tip: If your blog is multi-author and you genuinely need archives, block only the ?author=N parameter and keep /author/SLUG/ reachable. But make sure every slug is distinct from any login.