Technote

Security Practical

Series The REST API and going headless Part 6 of 8

Closing user enumeration: what REST hands out by default

/wp-json/wp/v2/users opens without login, and the slug it carries is usually the username. That leaves an attacker with only the password to find.

WordPress publishes a list of users who have authored posts, by default. Open /wp-json/wp/v2/users without logging in and you get names and a slug for each. On a default install, that slug is the username.

What changes once the username is known

Be precise about this: user enumeration is not a vulnerability in itself. It is a discount on the attacker’s costs. Logging in means guessing two things, a username and a password. Publish the username and it becomes a one-variable problem. Add the absence of login rate limiting and automated guessing becomes viable.

There is more than one leak.

Where usernames escape — close one and the rest remain

Closing it, in two layers

The first layer is to separate the username from the public name. This is the fundamental fix: however a name leaks, if it is not the login it does not reduce the attacker’s problem.

# make the slug (user_nicename) differ from the login
wp user update 1 --user_nicename=team --display_name="WPER Team"

The second layer is to close the routes. Remove the users collection for logged-out requests and cut off author-archive probing. Leave the logged-in case alone so the block editor’s author picker keeps working.

// Drop the user routes for logged-out requests only.
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;
} );

// If the site does not use author archives, stop the probing entirely.
add_action( 'template_redirect', function () {
	if ( is_author() || isset( $_GET['author'] ) ) {
		wp_safe_redirect( home_url( '/' ), 301 );
		exit;
	}
} );

That second snippet is only correct on a site that does not use author archives. Where several writers contribute and author pages are a content asset, this code deletes an asset. Before pasting in a hardening snippet, check whether the site actually uses the feature it removes.

And one layer at the server

Having closed it in the application, add the same rule at the server. Blocking unauthenticated /wp-json/wp/v2/users and ?author= in nginx means one layer survives a plugin overriding your filter or a theme change. Two layers of the same defence is not redundancy; it is what keeps a single missing layer from becoming an opening.

Verify it, too. Hit the routes from a logged-out browser or with curl and see what is still answering. Our free diagnostic tools include this in their checklist, so a route reopening after a migration or a plugin change gets caught rather than assumed closed.

Login protection and hardening more broadly are collected in the Security archive.

Next part

That closes the security run. Next is performance: which layer should cache API responses, and how to design the genuinely hard part — invalidation.

More on this topic

All technotes

Security Practical

Lead data is an asset and a liability at once

A contact list is a marketing asset and personal data at the same time. Collect the minimum, set a retention period, and actually delete when it expires.

Marketers 6 min read

₩270,000 · Join the program