Skip to content

User Enumeration Protection: Your Site Is Telling Strangers Your Username

The first time I watched a real user enumeration attempt scroll through Guard Dog’s activity log, what struck me was how boring it looked.

No failed logins. No lockouts. No errors. Just a patient sequence of requests — ?author=1, then ?author=2, then ?author=3 — from one IP address in a country I have never sent a newsletter to. Nothing broke. Nothing got blocked. The site answered every single one politely.

That is the part that gets missed, and it is why “User Enumeration Protection” lands so badly as a label in a settings screen. It doesn’t sound like a defense against anything. It sounds like a checkbox from a compliance form.

Enumeration is a fancy word for making a list

To enumerate is to count off, one at a time. That’s the whole concept.

A user enumeration attack is somebody building a list of the real usernames on your site. No password guessing. No exploit. No vulnerability in the strict sense of the word. Somebody asks your site a series of perfectly ordinary questions and writes down the answers.

It’s reconnaissance, not a break-in. Which is exactly why it’s easy to dismiss, and exactly why it matters.

A username is half the login

Every attack on a WordPress account has to answer two questions: who, and what’s the password. Enumeration answers the first one for free.

Once an attacker has a real username, brute-forcing stops being a guess about two unknowns and becomes a guess about one. Credential stuffing gets easier too — those breach dumps floating around are lists of usernames and passwords, and knowing that jsmith exists on your site tells someone which entries in the dump are worth trying. And it makes phishing sound real. “Hi Adam, there’s a problem with your administrator account” is a much better email when the sender knows both of those things are true.

It also tells an attacker who is worth the effort. A list of eight usernames where one of them is clearly the administrator is a much better target than a site where nobody knows if there are eight users or eight hundred.

WordPress isn’t being careless. It’s being helpful.

Here’s the thing that surprised me when I started closing these off: every place WordPress hands out a username, it does so for a reason somebody asked for.

Author archives exist so bylines have a page. The REST API users endpoint exists so the block editor can populate an author dropdown. oEmbed includes author fields so that when another site links to your post, the preview can say who wrote it. Login errors distinguish “we’ve never heard of that username” from “wrong password” so that real people can work out what they mistyped.

None of it is a bug. It’s a set of small, reasonable conveniences that add up, collectively, to a public directory.

The seven doors

Guard Dog covers seven of them, and they’re worth knowing by name even if you never touch a setting.

Author archives. Request ?author=1 and WordPress helpfully resolves the ID to that person’s archive and redirects to /author/their-username/. The username is now in the URL. Count up from 1 and you have the staff list.

The REST API users endpoint. /wp-json/wp/v2/users returns a JSON list of everyone who has published. No login required, on a default install.

Login error messages. “Unknown username” and “the password you entered for the user X is incorrect” are two different sentences, and the difference between them is a yes-or-no answer about whether that account exists.

Password reset. Same shape, different form. A site that replies “there is no user registered with that email address” has just confirmed which addresses are registered.

XML-RPC. The old remote-publishing interface is still enabled on a lot of sites, and methods like wp.getAuthors and wp.getUsers will simply hand over user lists.

oEmbed. When another site embeds one of your posts, the response WordPress sends back includes the author’s name and profile URL.

Registration errors. If you allow public sign-ups, “that username is already taken” turns your registration form into a lookup tool.

Seven doors, and a site owner who has never encountered the phrase has all seven standing open.

Protection isn’t a wall. It’s a straight face.

The instinct is to think protection means blocking things. Turn them off. Return 403s. Slam doors.

That’s the wrong shape, and done badly it’s worse than doing nothing — because a door that slams is still an answer. If ?author=1 returns “access denied” and ?author=47 returns a normal 404, congratulations: you’ve built a very convenient user detector out of your own security feature.

Real enumeration protection makes every answer the same answer.

Ask the REST API for the user list and you get an empty array, which is exactly what a site with nothing to show would return. Ask for ?author=1 and you get a 404, the same 404 as any page that was never there. Fail a login and you get “Invalid username or password,” whether or not that username exists. Ask to reset the password for an address the site has never seen and you get “If this email address is registered, you will receive a password reset link” — which is also what a real user sees, because a real user still gets the email.

The attacker’s list comes back the same length no matter who they ask about. The technique stops producing information. That’s the win, and if it’s working properly, none of your actual visitors will ever notice it exists.

Where it gets interesting

Two details from building this that I think are worth saying out loud.

The first is that blocking ?author=1 and blocking author archives are not the same decision. A multi-author publication needs /author/jane-doe/ to work — those pages have bylines, links, and SEO value attached to them. So Guard Dog only treats the raw numeric ?author=N form as a probe, and leaves the pretty author URLs alone. Protecting the site shouldn’t mean breaking the thing the site is for.

The second is a bug I shipped. Login error masking works by matching the error code WordPress returns rather than the text of the message, because message text changes with the site’s language and a text match would only ever work in English. That approach came out of fixing a real problem in 1.9.52, where configuring a custom generic login error message actually switched masking off instead of on — which meant a setting that looked like it was tightening security was quietly telling visitors which usernames were real. It’s fixed, and it’s a good reminder that on this particular feature, the failure mode is silent. Nothing breaks. It just stops working.

What I’d actually do

If you’re setting this up for the first time: flip the master switch on and leave every vector enabled, which is how Guard Dog arrives — all seven turn on the moment the feature is active. Administrators bypass all of it by default, so your own block editor and media library keep working.

Then log out, open a private window, and check the handful of things you care about — your author pages, a link preview, a password reset. Turn off exactly the one vector that broke something, if any did. Multi-author sites sometimes want author archives back. Headless setups need the REST API. Everything else can stay on.

Leave logging on either way. Guard Dog records every attempt as an enumeration_attempt event whether or not it blocked it, so you can look at your own logs and see whether anyone is actually probing you before you decide how aggressive to be. And leave auto-blocking on — twenty attempts in an hour puts that IP on the blacklist for a day, which handles the scanners without you doing anything.

It doesn’t save you. It just stops helping them.

I want to be honest about what this feature is worth.

User enumeration protection will not stop a determined attacker who has decided to target you specifically. Your name is on your bylines. It’s on LinkedIn. It might be in your theme’s Git history. Someone willing to spend an afternoon will find it.

What it stops is the industrialized version — the scanner hitting ten thousand WordPress sites tonight, taking whatever’s free, and moving on to the next one. That scanner isn’t going to spend an afternoon on you. It’s going to take the empty array and go.

That’s the whole calculation. Enumeration protection isn’t the feature that saves you; limiting login attempts and two-factor authentication do the saving. This one just declines to hand over the guest list on the way in.


Guard Dog’s user enumeration protection is free and covers all seven vectors described here. Full documentation is on guarddog.cc, and the plugin is available on WordPress.org.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *