Distinct error messages for "user not found" versus "wrong password" let an attacker enumerate valid email addresses with a single automated script, turning a minor information leak into a targeted credential-stuffing campaign. NIST 800-53 rev5 SI-11 (Error Handling) prohibits revealing system internals or data states in user-facing messages; AU-3 requires that sensitive detail be captured in server-side logs only. CWE-209 (Information Exposure Through an Error Message) and OWASP A05 (Security Misconfiguration) both flag this as a systematic vulnerability. Database exception strings leaked to the browser also reveal table names, column names, and query structure.
Medium because information-leaking errors aid enumeration and reconnaissance but require attacker interaction with the live system — they do not directly expose data without further exploitation.
Collapse all authentication failure branches into a single generic message. Log the specific cause server-side with the user identifier so your team retains forensic visibility.
// app/api/login/route.ts
export const POST = async (req: Request) => {
const { email, password } = await req.json()
try {
const user = await db.user.findUnique({ where: { email } })
const valid = user && await bcrypt.compare(password, user.passwordHash)
if (!valid) {
logger.warn({ email, found: !!user }, 'failed login attempt')
// Same message regardless of which branch failed
return Response.json(
{ error: 'Invalid email or password' },
{ status: 401 }
)
}
// ... issue session
} catch (err) {
logger.error({ err }, 'login error')
return Response.json({ error: 'An error occurred' }, { status: 500 })
}
}
Apply the same pattern to registration ("An account with that email already exists" leaks existence), password reset, and any form that queries user records.
ID: gov-fisma-fedramp.audit-accountability.error-messages-sanitized
Severity: medium
What to look for: Enumerate all API routes and form handlers. For each, examine error responses. Quote the actual error message strings returned to clients. Look for error messages that reveal details like "User with email already exists" (allows enumeration), database error messages, stack traces in responses, or hints about valid usernames/data structure. Check both client-side and server-side error handling. Count the number of endpoints with information-leaking errors vs total endpoints checked.
Pass criteria: Error messages are generic and don't reveal sensitive information across at least 100% of API routes examined. Instead of "User with email john@example.com not found", the response uses "Invalid email or password". Database errors are logged server-side but not exposed to client. Report the count of endpoints verified as safe.
Fail criteria: Any error message reveals information that could be exploited for enumeration, timing attacks, or data disclosure. Examples: "Username john doesn't exist", "Password is too weak (must have 1 uppercase)", database exception details.
Skip (N/A) when: Never — error message disclosure is a common vulnerability.
Detail on fail: Name specific endpoints or forms with problematic messages. Example: "POST /api/login returns different error for 'User not found' vs 'Invalid password' — enables username enumeration. 500 errors expose database exception messages to client."
Cross-reference: For comprehensive error page configuration and stack trace prevention, the Security Headers audit covers custom-error-pages and no-stack-traces checks.
Remediation: Use generic error messages in user-facing responses:
// app/api/login/route.ts (WRONG)
const user = await db.user.findUnique({ where: { email } })
if (!user) {
return Response.json({ error: 'User not found' }, { status: 404 })
}
// app/api/login/route.ts (CORRECT)
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 })
}
// Log detailed errors server-side only
if (!user) {
logger.warn(`Login attempt for non-existent email: ${email}`)
}
Catch all errors and return generic messages to the client:
export const POST = async (req: Request) => {
try {
// ... auth logic
} catch (error) {
logger.error('Login error', error) // Log with full details
return Response.json(
{ error: 'An error occurred. Please try again.' },
{ status: 500 }
)
}
}