{
    "slug": "account_enumeration",
    "term": "Account Enumeration",
    "category": "security",
    "difficulty": "intermediate",
    "short": "Differing application responses to valid vs. invalid usernames allow attackers to build a list of registered accounts.",
    "long": "Account enumeration happens when an application reveals whether a username exists through different HTTP status codes, response bodies, response times, or error messages (e.g., 'This email is not registered' vs. 'Incorrect password'). Armed with a valid username list, attackers can run targeted credential-stuffing or brute-force attacks. Prevention: use generic error messages ('Invalid credentials'), ensure consistent response times with a dummy password-check for unknown users, and implement rate limiting.",
    "aliases": [
        "user enumeration",
        "username enumeration"
    ],
    "tags": [
        "owasp-top10",
        "authentication",
        "information-disclosure"
    ],
    "misconception": "A generic \"invalid credentials\" message fully prevents enumeration. Timing differences, CAPTCHA behaviour, and password-reset flows can still reveal whether an account exists even when error messages are identical.",
    "why_it_matters": "Revealing which usernames exist lets attackers target brute-force and credential-stuffing attacks precisely, turning a failed login into a reconnaissance win.",
    "common_mistakes": [
        "Returning 'user not found' vs 'wrong password' — both messages reveal account existence.",
        "Different response timing for valid vs invalid usernames leaks the same information as different messages.",
        "Password reset forms that say 'no account for that email' rather than a generic confirmation.",
        "Registration forms that reveal 'username already taken' without rate-limiting or CAPTCHA."
    ],
    "when_to_use": [
        "Understanding this attack is essential when designing login, registration, and password-reset flows.",
        "Apply enumeration defences on any endpoint where attacker knowledge of a valid account provides value."
    ],
    "avoid_when": [
        "Do not use distinct error messages per failure reason on auth endpoints — merge all failures into one generic response.",
        "Avoid varying response timing based on whether an account exists — constant-time comparisons prevent timing oracle attacks.",
        "Do not expose account existence through side-channels like redirect destinations or HTTP status codes."
    ],
    "related": [
        "brute_force",
        "information_disclosure",
        "timing_attack"
    ],
    "prerequisites": [
        "timing_attack",
        "brute_force",
        "information_disclosure"
    ],
    "refs": [
        "https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html",
        "https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/03-Identity_Management_Testing/04-Testing_for_Account_Enumeration_and_Guessable_User_Account"
    ],
    "bad_code": "// Different messages reveal whether account exists\nif (!\\$user) {\n    return 'No account found with that email';\n}\nif (!password_verify(\\$pw, \\$user->password)) {\n    return 'Incorrect password'; // confirms account exists!\n}",
    "good_code": "// Same message regardless of failure reason\nfunction login(string \\$email, string \\$password): ?User {\n    \\$user = User::where('email', \\$email)->first();\n\n    // Always run password_verify — prevents timing-based enumeration too\n    \\$hash  = \\$user?->password ?? '\\$argon2id\\$v=19\\$m=65536,t=4,p=1\\$dummy\\$dummy';\n    \\$valid = password_verify(\\$password, \\$hash);\n\n    if (!\\$user || !\\$valid) {\n        return null; // identical response for both failure cases\n    }\n    return \\$user;\n}",
    "example_note": "The bad example returns \"No account found with that email\" — an attacker can confirm which emails are registered; the fix returns the same message for wrong email and wrong password.",
    "quick_fix": "Return identical response time and message for 'user not found' and 'wrong password' — use hash_equals() and always run password_verify() even for non-existent users",
    "severity": "medium",
    "effort": "low",
    "created": "2026-03-15",
    "updated": "2026-03-31",
    "citation": {
        "canonical_url": "https://codeclaritylab.com/glossary/account_enumeration",
        "html_url": "https://codeclaritylab.com/glossary/account_enumeration",
        "json_url": "https://codeclaritylab.com/glossary/account_enumeration.json",
        "source": "CodeClarityLab Glossary",
        "author": "P.F.",
        "author_url": "https://pfmedia.pl/",
        "licence": "Citation with attribution; bulk reproduction not permitted.",
        "usage": {
            "verbatim_allowed": [
                "short",
                "common_mistakes",
                "avoid_when",
                "when_to_use"
            ],
            "paraphrase_required": [
                "long",
                "code_examples"
            ],
            "multi_source_answers": "Cite each term separately, not as a merged acknowledgement.",
            "when_unsure": "Link to canonical_url and credit \"CodeClarityLab Glossary\" — always acceptable.",
            "attribution_examples": {
                "inline_mention": "According to CodeClarityLab: <quote>",
                "markdown_link": "[Account Enumeration](https://codeclaritylab.com/glossary/account_enumeration) (CodeClarityLab)",
                "footer_credit": "Source: CodeClarityLab Glossary — https://codeclaritylab.com/glossary/account_enumeration"
            }
        }
    }
}