Distinct error messages for 'email not found' versus 'wrong password' allow automated enumeration of every registered email address on your platform (CWE-204, CWE-203, CAPEC-196). The same applies to password reset endpoints that return 404 for unregistered emails. OWASP A07 covers this as an authentication information disclosure failure. Beyond security, leaking account existence violates user privacy — subscribers who registered with a personal email may not want their account existence confirmed to anyone who asks. A uniform response eliminates both the security and privacy exposure at zero cost.
Medium because account enumeration requires no credentials and enables targeted phishing and credential stuffing campaigns against confirmed accounts.
Return identical status codes and message text regardless of whether the email exists:
// Unified response — same message for missing email and wrong password
const user = await db.user.findUnique({ where: { email } })
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return Response.json({ error: 'Invalid email or password' }, { status: 401 })
}
// Password reset: fire-and-forget, always return the same message
await sendResetEmailIfExists(email)
return Response.json({
message: 'If this email is registered, you will receive a reset link shortly.'
})
Also normalize response time: a fast path (user not found, no hash compare) reveals account absence through timing. Add a constant-time delay or perform a dummy bcrypt compare even when the user does not exist.
ID: saas-authentication.auth-flow.auth-errors-no-email-reveal
Severity: medium
What to look for: Examine login, password reset, and account lookup endpoints. Check the error messages and response bodies returned for (a) an email that doesn't exist, and (b) an email that exists with a wrong password. Also check the password reset endpoint: does it confirm or deny whether the email is registered? Look for distinct error strings like "Email not found" vs. "Wrong password" which reveal account existence. Count all instances found and enumerate each.
Pass criteria: Login returns the same error message and HTTP status whether the email doesn't exist or the password is wrong. Password reset endpoint returns a generic "If this email is registered, you'll receive a reset link" response regardless of whether the email exists. At least 1 implementation must be confirmed.
Fail criteria: Login returns different error messages or response times that reveal whether an email is registered. Password reset endpoint confirms or denies whether an email exists.
Skip (N/A) when: No email/password login or no password reset flow. Signal: OAuth-only authentication, no credential-based login.
Detail on fail: "Login endpoint returns 'Email not found' for unknown emails and 'Incorrect password' for known emails — allows email enumeration" or "Password reset at /api/auth/reset-password returns 404 for unregistered emails, confirming account non-existence".
Remediation: Distinguishing between "email not found" and "wrong password" allows attackers to enumerate which email addresses have accounts on your service. Use a uniform response:
// Correct: same response regardless
const user = await db.user.findUnique({ where: { email } })
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return Response.json(
{ error: 'Invalid email or password' },
{ status: 401 }
)
}
// Password reset: same response whether email exists or not
await sendResetEmailIfExists(email) // Fire-and-forget
return Response.json({
message: 'If this email is registered, you will receive a reset link shortly.'
})
Also add a consistent time delay to prevent timing-based enumeration where a missing-user response is faster than a hash-compare response.