CWE-206, Observable Internal Behavioral Discrepancy, applies when a product combines several behaviors into a single result but lets outsiders see the individual behaviors. That visibility reveals internal state or decision points.
MITRE's own example is the one most developers know. A login has one outcome, success or failure. Internally it does two things: check whether the username exists, then check the password. If the product behaves differently depending on whether the username exists, the attacker only has to work on the password.
How it happens
The seam between steps shows. Examples from the record:
- CVE-2003-0190: the product sends an error immediately when the user does not exist, instead of waiting until the password is provided, allowing username enumeration.
- CVE-2005-2025: valid group names can be enumerated because the product responds if the name is valid and does not respond if not.
- CVE-2001-1497: a GUI lets attackers tell alphanumeric from non-alphanumeric characters in a password, reducing the search space.
- CVE-2002-2031: file existence leaks by watching whether an onerror handler fires.
What an attacker gains
Confidentiality and access control are affected: the attacker reads application data and bypasses a protection mechanism. Each leaked step cuts the guessing space.
How to find it
- For every multi-step check, list the steps and ask whether each one can fail visibly on its own.
- Test with a valid user and a bad password, then an invalid user and any password. Compare timing as well as content.
- Look for early returns. An error sent before all input has been collected is a sign.
How to fix it
MITRE's mitigation is to set up generic response pages for error conditions. The page should not disclose success or failure of a sensitive operation. A login page should not confirm that the account is right and the password wrong, because confirming the account makes brute force easier.
In practice: collect all input first, run every check, then return one answer for any failure.
user = db.find_user(email)
password_ok = check_password(user, password) if user else fake_check(password)
if not (user and password_ok):
return error("Invalid email or password", 401)
The fake_check stands in for a dummy hash comparison so the missing-user path costs about the same time.
Quick check
Why does an early error for a missing user matter?
It tells the attacker the username is invalid before any password is tried, so they can sort real accounts from fake ones.