WordPress admin username exposure via wp-json REST API

WordPress REST API exposes admin usernames at /wp-json/wp/v2/users by default. Attackers use this for targeted brute-force. Here's how to detect and stop it.

WordPress wp-json username exposure via REST API

WordPress's REST API, enabled by default since version 4.7, makes admin usernames publicly discoverable at /wp-json/wp/v2/users. Combined with the old ?author=1 redirect technique, username enumeration requires no credentials and no special tooling — just a curl command.

The /wp-json/wp/v2/users endpoint

WordPress exposes an authenticated user list through its REST API. The endpoint returns JSON objects for each user, including their name (display name) and slug (login name). For sites where the admin hasn't changed their username from admin or where the display name matches the login name, this is a direct credential.

curl -s https://yourdomain.com/wp-json/wp/v2/users | python3 -m json.tool

A typical response looks like this:

[
  {
    "id": 1,
    "name": "site-admin",
    "slug": "site-admin",
    "link": "https://yourdomain.com/author/site-admin/",
    ...
  }
]

The slug field is the WordPress username. This is what attackers use as the first half of a brute-force pair.

The ?author=1 redirect method

An older technique that still works on many sites: request https://yourdomain.com/?author=1 and follow the redirect:

curl -s -I https://yourdomain.com/?author=1 | grep -i location

WordPress redirects to the author archive URL, which typically contains the username in the path: /author/site-admin/. Increment the ID to enumerate all registered users. This method works even when the REST API user endpoint is disabled, because it relies on the author archive routing.

What attackers do with exposed usernames

Once an attacker knows the administrator username, brute-force attacks become far more efficient. Instead of testing username/password combinations across the full space, they can target one specific username with a large password dictionary against /wp-login.php or, worse, via XML-RPC's multicall method (see: the XML-RPC security article).

Exposed usernames also enable:

  • Credential stuffing — checking breached password lists against the known username. If the site admin reused a password from a previous breach, it often succeeds.
  • Social engineering — the exposed name field reveals the admin's display name, which can be used in targeted phishing.
  • User enumeration for multi-user sites — WooCommerce stores and membership sites may expose hundreds of customer usernames, enabling bulk credential attacks.

How to disable the users REST API endpoint

Via functions.php (child theme or custom plugin)

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

Add this to your child theme's functions.php or a site-specific plugin. Do not edit the parent theme's functions.php — it will be overwritten on theme updates.

Require authentication for user endpoint

An alternative that preserves the endpoint for authenticated requests (useful if you have a headless frontend that needs user data):

add_filter('rest_endpoints', function($endpoints) {
    if (!is_user_logged_in()) {
        if (isset($endpoints['/wp/v2/users'])) {
            $endpoints['/wp/v2/users'][0]['permission_callback'] = function() {
                return current_user_can('list_users');
            };
        }
    }
    return $endpoints;
});

Blocking the ?author= redirect

At the web server level (nginx), redirect author archive requests to 404 or the homepage:

if ($args ~ "author=\d") {
    return 301 /;
}

Or handle in WordPress (slower — PHP still executes):

add_action('template_redirect', function() {
    if (is_author()) {
        wp_redirect(home_url(), 301);
        exit;
    }
});

Username hardening beyond enumeration prevention

  • Change the admin username — if user ID 1 has username admin, create a new admin account with a non-obvious username, transfer content ownership, then delete the admin account. WordPress does not support renaming usernames natively.
  • Use distinct display names — set the display name to something different from the login name (Settings → Users → Your Profile → Display name publicly as). The REST API returns display name, not login name, but the slug field still exposes the login name.
  • Enable 2FA — username exposure matters far less if the login requires a second factor.
  • Implement login rate limiting — at the web server level, not just plugin level.

Common mistakes

  • Disabling REST API entirely — the WordPress REST API is used by Gutenberg editor, many plugins, and legitimate headless setups. Disabling it completely breaks things. Restrict the users endpoint specifically.
  • Blocking /wp-json but forgetting the ?author= redirect — two separate vectors, require two separate fixes.
  • Using display name as login name — WordPress's default for new accounts uses the username as the display name. Change display names to first name, full name, or a non-login identifier.

Orb44 detects username exposure via both the REST API endpoint and the author redirect method, and flags it as part of WordPress-specific security scanning.

FAQ

Does disabling the users endpoint break anything?

The Gutenberg editor and standard WordPress plugins don't require the public users endpoint. Some headless WordPress setups (Next.js frontend pulling from the REST API) may use it — review your architecture before disabling. The "require authentication" approach above is safer for headless deployments.

Is username exposure a critical vulnerability?

On its own, it's a low-to-medium severity information disclosure. Combined with weak or reused passwords and no 2FA or rate limiting, it becomes a direct path to site compromise. Fix the exposure and the underlying authentication weaknesses together.

Orb44 Journal · all posts