Skip to content

Discovering valid usernames

CWE-204CWE-203OWASP A07:2021Updated September 4, 20265 min read

When an application responds differently to an existing account than to a non-existent one, an attacker can establish which usernames are valid. That difference sits not only in the error message but also in status codes, redirects and response times. With a valid list, every follow-up attack becomes more targeted and cheaper.

A login screen that helpfully reports the password is wrong is also confirming that the username is right. That is convenient for anyone who mistypes and valuable for anyone building a list. This article explains along which routes that difference becomes visible, usually not only through the error message, and how to make the responses identical without leaving your users in the dark.

What is username enumeration?

Username enumeration is establishing which accounts exist in a system by comparing the application’s behaviour. The attacker needs no password and does not have to break in anywhere; they infer the information from the difference between two responses.

That difference need not sit in text. It can be a differing HTTP status code, a different redirect, a slightly longer response time, a difference in the length of the response, or the appearance of a captcha. Any observable distinction between “this account exists” and “this account does not exist” is usable.

It is like ringing the bell at an apartment block where one name produces a buzz and another produces nothing. You get in nowhere, but after fifteen minutes you know exactly who lives there. And that is the information with which the actual attack begins.

Where does the difference sit?

The login page is the best-known place, but rarely the only one.

Vulnerable:

app.post('/login', async (req, res) => {
  const user = await users.findByEmail(req.body.email);
  if (!user) {
    return res.status(404).send('This email address is not known to us');
  }

  const ok = await bcrypt.compare(req.body.password, user.hash);
  if (!ok) {
    return res.status(401).send('The password is incorrect');
  }
  // ...
});

Information leaks here in three ways at once: the text differs, the status code differs (404 against 401), and the response time differs, because bcrypt.compare runs only when the account exists. That last one is the most stubborn, because it survives after the first two are fixed. An attacker simply runs a list through it:

info@company.com      → 404, 12 ms   → does not exist
j.walker@company.com  → 401, 180 ms  → exists

The same mistake often recurs in the password reset function (“there is no account with this address”), at registration (“this address is already in use”) and when changing an email address.

Safe:

const DUMMY_HASH = '$2b$12$c8y3Nq0Wl0mVQe1n6JbEZeQmQm2Yy8kQF9nZ0Xf1yqk8dK1vI0Iuq';

app.post('/login', async (req, res) => {
  const user = await users.findByEmail(req.body.email);

  // Always compare, even without an account: equal computation time
  const hash = user?.hash ?? DUMMY_HASH;
  const ok = await bcrypt.compare(req.body.password, hash);

  if (!user || !ok) {
    await randomDelay();
    return res.status(401).send('The combination of email address and password is incorrect');
  }
  // ...
});

The response is now identical in every respect: the same text, the same status code and, because a hash comparison runs even for an unknown account, virtually the same response time. The same rule applies to the password reset function, with a neutral confirmation:

app.post('/forgot-password', async (req, res) => {
  const user = await users.findByEmail(req.body.email);
  if (user) await sendResetMail(user);   // silently skipped otherwise

  res.send('If this address is known to us, you will receive an email within a few minutes.');
});
Do not forget the side channels. A captcha that appears only for existing accounts, an account lockout that only triggers then, or a two-factor step shown only for valid users, reveals exactly the same thing as a differing error message, even when the text itself is perfectly neutral.

What is the impact of username enumeration?

As a standalone finding this is a matter of low severity: no access is gained and no data from the accounts leaks. Its significance lies in what it enables.

With a verified list of existing accounts, every follow-up attack becomes more efficient. A password attack that first had to work through thousands of non-existent addresses now targets only real accounts, which raises the chance of success and lowers the chance of detection. With password spraying, trying one common password across many accounts, such a list is even the main precondition.

There is also a direct abuse unrelated to logging in. The confirmation that someone has an account is sensitive information in itself. With a medical, legal, financial or politically sensitive service, the existence of an account can reveal more than its contents. And for targeted phishing it is a gift: an email referring to a service the recipient actually uses is considerably more convincing.

How do you detect username enumeration?

A tester sends two series of requests: one with addresses certain to have an account and one with addresses that do not exist. The responses are then compared on everything observable: the text, the status code, the length of the response, the headers, the redirect and the response time.

That happens not only on the login page. The surrounding functions are more often vulnerable: forgot password, registration, changing an email address, invitations, and API endpoints that check the availability of a username for the frontend. That last category is a known one: an endpoint that reports during a registration form whether a name is still free is by definition an enumeration interface. AssistSec also measures response times statistically, because a timing difference that disappears in a single measurement becomes clearly visible over hundreds of requests, and that is exactly how an attacker would approach it.

How do you prevent username enumeration?

  • Always give the same message and the same status code at login, regardless of whether the account exists.
  • Run a hash comparison even for an unknown account, so the response time does not reveal what happened.
  • Confirm neutrally at password reset that an email has been sent if the address is known.
  • Do not state at registration that an address is already in use; explain that in the email to that address.
  • Make sure captchas, delays and lockouts trigger identically for existing and non-existent accounts.
  • Avoid endpoints that check the availability of a username or email address, or restrict them strictly.
  • Limit the number of attempts per IP address and per time unit, so large-scale enumeration stands out and stalls.
  • Monitor and alert on an unusual number of failed logins with varying usernames.

Sources

Frequently asked questions

Does this matter if usernames are email addresses anyway?

It makes it less serious, but not unimportant. An attacker may know the address but not whether an account exists with you. That confirmation is valuable: it makes targeted phishing more credible and narrows a password attack to accounts that actually exist.

How do I prevent enumeration at registration?

Never state directly that an address is already in use when creating an account. Confirm instead that an email has been sent, and explain in that email whether it concerns a new account or an existing one. The information then reaches only whoever controls the mailbox.

Why does response time matter?

Because an application usually verifies the password for an existing account and does nothing for an unknown one. That computation takes measurable time, often tens of milliseconds. An attacker making thousands of attempts sees that difference clearly, even when the error message is identical.

Is this still a problem with two-factor authentication?

The severity drops, because a valid username and a guessed password are then not enough. But the list stays useful for targeted phishing and for attacks on the second factor itself, such as bombarding someone with push notifications until they approve one.

Related articles

Press / to search · Esc