Safeguard
Application Security•

ASVS 5.0 V6.3.8: Do Not Reveal Which Users Exist

Login, registration and password reset must not reveal whether an account exists, through messages, status codes or response timing. Level 3, with code and tests.

Safeguard Security Team
4 min read

ASVS 5.0 requirement V6.3.8 says an attacker must not be able to tell which usernames or emails belong to real accounts by watching how your authentication flows fail. That covers error text, HTTP status codes and response times, and it applies to login, registration and forgot password alike. It is a Level 3 requirement.

Why it matters

A list of valid accounts is the first input to credential stuffing, targeted phishing and password spraying. If "no such user" and "wrong password" look different, an attacker can test millions of emails and keep only the real ones. Your rate limits then protect fewer accounts than you think, because every guess is aimed at an account that exists.

Enumeration also leaks business facts. Registration that says "this email is already registered" tells anyone whether a named person uses your product.

How to implement it

Three channels leak: the message, the status code, and the clock. Fix all three.

Messages and codes. Return one generic response for every failure on login. For forgot password, always answer "If that address has an account, we sent instructions" with the same status code. For registration, respond the same way whether or not the email exists, and send the real outcome by email: a welcome link to a new user, or a "you already have an account" note to the existing one.

Timing. The classic leak is skipping the password hash when the user is missing. Always do the same work. Hash against a fixed dummy value when no user is found.

import hmac
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

ph = PasswordHasher()
DUMMY_HASH = ph.hash("dummy-password-for-timing")

def login(email: str, password: str):
    user = db.find_user_by_email(email.strip().lower())
    stored = user.password_hash if user else DUMMY_HASH
    try:
        ph.verify(stored, password)
        ok = user is not None
    except VerifyMismatchError:
        ok = False
    if not ok:
        return {"error": "Invalid email or password"}, 401
    return start_session(user)

For forgot password and registration, do the slow part (email sending) in a background queue so the HTTP response returns at the same speed in both branches.

def forgot_password(email: str):
    queue.enqueue(send_reset_if_account_exists, email.strip().lower())
    return {"message": "If that address has an account, we sent instructions."}, 202

Also check the side doors: account lockout messages, MFA prompts that only appear for real users, and "login with SSO" redirects triggered by a known email domain.

How to test it

  1. Send a login request for a real account with a wrong password and one for a made up address. Diff the full response: status, body, headers, cookies.
  2. Repeat 200 times each and compare median and 95th percentile latency. A gap of more than a few milliseconds with a stable network is a signal.
  3. Run the same pair against registration and forgot password.
  4. Trigger lockout on a real account and confirm a fake account shows the same text after the same number of attempts, or that lockout text is generic.

Common mistakes

  • A generic message but a different status code (401 versus 404).
  • The same body but an extra Set-Cookie or a different redirect for real users.
  • Skipping hashing for unknown users.
  • Sending the reset email inline, so existing accounts respond slower.
  • Leaking through a separate endpoint such as "check email availability" on the signup form.

Try it yourself

A dynamic scan finds differing responses to valid and invalid users without you writing the harness.

Click through it at your own pace, or open it full screen.

Quick check

Is a generic error message enough to meet V6.3.8?

No. Status codes and response time must also match, and registration and password reset are in scope too.

Why hash a dummy value for unknown users?

Password hashing is slow on purpose. Skipping it for a missing account makes failures for fake users visibly faster than for real ones.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.