Safeguard
Application Security•

CWE-204 Observable Response Discrepancy: Stop User Enumeration

Different responses for valid and invalid input let attackers map your users and internals. CWE-204 explained, with a fix for the login flow.

Safeguard Security Team
3 min read

CWE-204, Observable Response Discrepancy, is what happens when a product answers requests differently in a way that reveals internal state to someone outside the intended control sphere. The classic case is a login or reset form that treats "no such user" differently from "wrong password."

Any application with accounts, IDs or files that a stranger can probe is exposed to it.

How it happens

The code branches on something the user should not learn, and the branch shows up in the response. MITRE's observed examples are almost all enumeration bugs:

  • CVE-2001-1483: enumeration of valid usernames based on inconsistent responses.
  • CVE-2001-1528: account number enumeration via inconsistent responses.
  • CVE-2004-2150: user enumeration via discrepancies in error messages.
  • CVE-2002-2094: ".." attacks that monitor error responses, which overlaps with directory traversal.

A vulnerable login looks like this:

# Vulnerable: the message tells the caller which half was wrong
user = db.find_user(email)
if user is None:
    return error("No account with that email", 404)
if not check_password(user, password):
    return error("Incorrect password", 401)

What an attacker gains

MITRE lists confidentiality and access control impact: the attacker can read application data and bypass a protection mechanism. In practice, a list of confirmed usernames turns a broad guessing job into a targeted one, and it feeds phishing and credential stuffing.

How to find it

  1. Send a request with a valid identifier and one with an invalid identifier. Diff the status code, body, headers and response time.
  2. Do this on every flow that takes an identifier: login, password reset, registration, invite and lookup endpoints.
  3. Probe file and path parameters and compare errors for "does not exist" against "not allowed."

How to fix it

  • Return one generic response for every failure path. "Invalid email or password" works for both cases.
  • For password reset, say "If that account exists, we sent an email" regardless of the result.
  • Use the same status code and the same shape of body. Watch timing too, since a skipped password hash on a missing user is measurable.
  • Keep detail in server logs, not in what the caller sees. Do not put passwords in logs.
  • Compartmentalize so sensitive data stays inside a trust boundary, and apply least privilege.
user = db.find_user(email)
ok = user is not None and check_password(user, password)
if not ok:
    return error("Invalid email or password", 401)

Quick check

Is returning 404 for missing users and 401 for bad passwords a problem?

Yes. The two codes together answer the question "does this account exist?" for anyone who asks.

Do generic messages fully fix it?

Only if the rest of the response matches, including status, headers and timing.

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.