User Enumeration Testing: Finding Username and Email Disclosure Vulnerabilities

User enumeration is not a direct account takeover — but it is the first step. Before an attacker can perform credential stuffing, brute force a password, or send a targeted phishing email, they need to know which accounts exist. Applications that reveal whether a username or email is registered give attackers a free lookup service against your user database.

The vulnerability appears in multiple places: login forms, registration flows, password reset endpoints, and public-facing APIs. It shows up as different response bodies, different HTTP status codes, or measurable differences in response timing. This guide covers how to find and document it across all of them.

Why Enumeration Matters

Enumeration vulnerabilities appear in a supporting role in most account-targeted attacks:

  • Credential stuffing preparation — Attackers validate known email/password pairs against confirmed accounts only, reducing noise and avoiding lockouts on non-existent accounts.
  • Targeted brute force — Once an account is confirmed, password brute force or spraying can be precisely targeted.
  • Phishing — Confirmed email-to-account mapping enables targeted phishing campaigns with spoofed sender addresses that match the service.
  • Privacy disclosure — For sensitive applications (health, finance, legal), confirming that a specific person has an account may itself be a privacy violation, regardless of further exploitation.

Login Form Enumeration

The login endpoint is the most common source of enumeration. The vulnerability occurs when the application returns different messages for "username doesn't exist" versus "wrong password."

Testing the login error message

Submit three requests and compare the responses:

# Request 1: Non-existent username
POST /login
username=definitely_not_real_user_xyz&password=wrongpassword

# Request 2: Valid username, wrong password
POST /login
username=admin&password=wrongpassword

# Request 3: Valid username, correct password (to confirm the account)
POST /login
username=admin&password=correctpassword

Compare responses for Request 1 and Request 2. Vulnerable patterns include:

Request Vulnerable Response Secure Response
Unknown username "User not found" "Invalid credentials"
Wrong password "Incorrect password" "Invalid credentials"
Locked account "Account locked" "Invalid credentials"
Disabled account "Account disabled" "Invalid credentials"

Also compare HTTP status codes. Some applications return 404 for unknown usernames and 401 for wrong passwords — a status code difference is sufficient for enumeration even if the response bodies are identical.

Testing response time differences

Timing-based enumeration is more subtle but equally effective. When a username doesn't exist, the application may skip the password hashing step entirely (since there's no hash to compare), returning faster than for a valid username with a wrong password. The difference can be hundreds of milliseconds with bcrypt or Argon2.

# Measure response time differences using curl
time curl -s -X POST https://target.com/login \
  -d "username=nonexistent&password=test" > /dev/null

time curl -s -X POST https://target.com/login \
  -d "username=admin&password=wrongpassword" > /dev/null

# A consistent difference of 200ms+ is exploitable for enumeration
# Use Burp Intruder with response time column for bulk testing

For reliable timing measurements, run each request 10+ times and average the results to account for network jitter. Consistent deltas of 100ms or more are reportable as enumeration via timing side channel.

Registration Form Enumeration

Registration endpoints often confirm whether an email is already registered. This is actually more useful to attackers than login enumeration — no rate limiting typically applies to registration attempts, and the application is designed to be informative about uniqueness constraints.

Inline enumeration

Submit a registration form with a known email address. If the response confirms the email is taken ("An account with this email already exists"), that's enumeration. Compare with an unknown email address to confirm the difference.

Async validation

Many modern applications check email uniqueness via an API call as the user types (onBlur or debounced keypress). Intercept the traffic while entering an email into the registration field:

# Look for XHR/fetch requests like:
GET /api/users/check?email=target@example.com

# Or:
POST /api/validate
{"email": "target@example.com"}

# Response variations:
{"available": false}  # Email is registered
{"available": true}   # Email is not registered

These async check endpoints often lack the rate limiting applied to the full registration form. They may also accept arbitrary emails without any CSRF token, making bulk enumeration trivial.

Username uniqueness checks

The same pattern applies to username fields. Applications that confirm "username is taken" vs. "username is available" in real time are providing a lookup service against every registered username.

Password Reset Enumeration

Password reset is covered in depth in the password reset testing guide, but the enumeration angle deserves specific attention here. The canonical error is:

  • Known email: "If this email exists, you will receive a link." (Status 200)
  • Unknown email: "Email address not found." (Status 200 or 404)

Both responses should be identical if the application is properly implemented. Test the status code, response body, and response time. Also check whether the application sends an email at all for unknown addresses — if you observe a delay for known accounts (email delivery) but an immediate response for unknown accounts, that's a timing-based enumeration vector.

API Endpoint Enumeration

REST APIs often expose user lookup functionality that can be abused for enumeration.

Direct user ID or username lookup

# User profile endpoints
GET /api/users/admin           # 200 = exists, 404 = doesn't
GET /api/users/target@email.com
GET /api/profile?user=admin

# Account existence endpoint (sometimes explicit)
GET /api/accounts/exists?username=admin

Password reset API

# Reset request API — compare for known vs unknown
POST /api/auth/forgot-password
{"email": "known@example.com"}    # 200

POST /api/auth/forgot-password
{"email": "unknown@example.com"}  # 404 or different body

OAuth / SSO endpoint enumeration

SSO login flows can leak account existence. When a user attempts to authenticate via an IdP, the application may redirect to the IdP with the user's email as a hint. If the email doesn't exist, the application may return a different error than "wrong password" — revealing that no account is linked to that email at the IdP level.

Indirect Enumeration Sources

Profile pages and public usernames

Applications with public profile pages (social features, public portfolios) may allow username enumeration simply by accessing the profile URL:

GET /users/alice    # 200 = exists
GET /users/nobody   # 404 = doesn't exist

This is intentional by design in many cases, but should still be documented if the application is not supposed to be a social platform and the username reveals sensitive account existence information.

Error messages in headers

Some applications include diagnostic information in HTTP response headers. Check for:

  • X-Error, X-Debug, or custom error headers
  • WWW-Authenticate headers with realm information
  • Verbose Server or X-Powered-By headers that might expose version-specific error behavior

Account lockout behavior

If the application locks accounts after N failed attempts, the lockout response itself reveals account existence. Submit 10 failed login attempts against a known account and observe the response change to "Account locked." Submit the same against an unknown account — if the response never changes to "Account locked," the lockout confirms the known account exists.

Testing with Burp Suite

For bulk enumeration testing, Burp Intruder is the right tool:

  1. Capture a login or registration request in Proxy.
  2. Send to Intruder. Set the username/email field as the payload position.
  3. Load a wordlist of common usernames or known email addresses.
  4. Start the attack. Sort results by Response Length or Response Code.
  5. Look for outliers — responses that differ from the baseline indicate account existence.

Enable the "Response received" column to identify timing differences. For time-based enumeration, sort by response time and look for statistically consistent outliers.

Documenting the Finding

When reporting user enumeration, provide clear evidence:

  • Exact request and response for known account
  • Exact request and response for unknown account
  • The specific difference: response body, status code, timing delta, or header content
  • Risk context: does the application handle sensitive data? Is it a B2B SaaS where knowing a company has an account is sensitive? Is the username the user's real name or email?

Severity varies by context. Enumeration on a generic SaaS with public sign-up is usually Medium. Enumeration on a healthcare portal, a legal discovery platform, or an anonymous reporting tool can be High or Critical due to the privacy implications of confirming account existence.

What Ironimo Catches

Ironimo's scanner systematically probes login, registration, and password reset endpoints for enumeration vectors. It submits requests with known-nonexistent usernames and compares response bodies, status codes, and response times against a baseline. Async validation endpoints are identified during crawl and tested separately. API endpoints that accept user identifiers are checked for differential responses. Findings include the specific request-response pairs that demonstrate the enumeration, along with the risk context for the application type.

User Enumeration Testing Checklist

  1. Map all authentication endpoints — Login, registration, password reset, account unlock, and any async validation APIs.
  2. Test login form — response body — Compare error messages for unknown username vs. wrong password. Both should return identical messages.
  3. Test login form — status codes — Verify status codes are identical for both failure cases (401 or 200, but not 404 for one and 401 for the other).
  4. Test login form — timing — Measure response times for known vs. unknown accounts. Run 10+ iterations and average. Flag consistent differences of 100ms+.
  5. Test registration — inline validation — Enter known email addresses into the registration form and capture async validation requests.
  6. Test registration — form submission — Submit full registration with known email; compare response to submission with unknown email.
  7. Test password reset — response body — Submit reset for known vs. unknown email and compare responses.
  8. Test password reset — timing — Measure response time for known vs. unknown accounts.
  9. Test API user lookup endpoints — Check /api/users/{id}, /api/profile?user=, and similar endpoints for differential responses.
  10. Test account lockout behavior — Verify that lockout responses don't reveal account existence relative to rate-limiting responses for non-existent accounts.
  11. Check public profile pages — Confirm whether profile URL existence reveals user account data that should be private.
  12. Check SSO/OAuth flows — Test whether the IdP redirect or error messages reveal account existence at the identity provider level.

Catch enumeration vulnerabilities before attackers do

Ironimo scans your authentication endpoints systematically — including timing-based enumeration that manual testers often overlook. On-demand, no pentester invoice.

Start free scan
← Back to blog