๐Ÿ›ก OWASP Lab
A07:2021 High

Identification & Authentication Failures

Theory

Formerly called Broken Authentication, this category covers every weakness in how the application proves who a user is and maintains that identity across requests. A successful attack lets an adversary impersonate another user โ€” temporarily or permanently. It sits at #7 (2021).

Types of Authentication Failures

  • Weak session tokens โ€” tokens derived from predictable values (MD5(username), timestamp, user_id). An attacker who can guess or compute a token hijacks the session
  • Username enumeration โ€” different error messages for "user not found" vs "wrong password" allow attackers to validate email/username lists before targeted attacks
  • No brute-force protection โ€” unlimited login attempts; attackers can automate credential stuffing from data breach dumps (e.g. 8.4 billion credentials in RockYou2021)
  • Credential stuffing โ€” testing breached credentials (username:password pairs) from one site against another; works because users reuse passwords across sites
  • Missing MFA โ€” high-value accounts (admin, finance, DevOps) without multi-factor authentication are trivially taken over by phishing or credential stuffing
  • Session fixation โ€” session ID not rotated after login; attacker plants a known session ID before login and inherits the authenticated session
  • Long-lived tokens โ€” JWTs or API keys that never expire; a leaked token is valid indefinitely

Weak Session Token โ€” Vulnerable Code

import hashlib

# VULNERABLE โ€” session is MD5(username); attacker computes it trivially
def create_session(username: str) -> str:
    return hashlib.md5(username.encode()).hexdigest()   # deterministic, no randomness

# MD5("admin") = 21232f297a57a5a743894a0e4a801fc3
# MD5("alice")  = 6384e2b2184bcbf58eccf10ca7a6563c
# Any known username โ†’ instant token forgery

Username Enumeration โ€” Vulnerable Code

# VULNERABLE โ€” different messages reveal whether a user exists
@app.post("/login")
def login(username: str, password: str):
    user = db.get_user(username)
    if not user:
        return {"error": "User not found"}           # confirms username does NOT exist
    if not check_password(password, user.hash):
        return {"error": "Invalid password"}         # confirms username DOES exist
    return {"token": create_session(user)}

Fixed Code

import secrets
from datetime import datetime, timedelta

# FIXED โ€” cryptographically random session token
def create_session(user_id: int) -> str:
    token = secrets.token_hex(32)   # 256 bits of randomness; unguessable
    sessions[token] = {"user_id": user_id, "expires": datetime.utcnow() + timedelta(hours=8)}
    return token

# FIXED โ€” identical error message regardless of whether user exists
@app.post("/login")
def login(username: str, password: str):
    user = db.get_user(username)
    # Always run check_password even if user is None (prevents timing attacks)
    # Use bcrypt.checkpw with a dummy hash for non-existent users
    dummy_hash = "$2b$12$invalidhashfortimingprotection"
    valid_hash = user.password_hash if user else dummy_hash
    password_ok = bcrypt.checkpw(password.encode(), valid_hash.encode())
    if not user or not password_ok:
        return {"error": "Invalid username or password"}  # same message either way
    return {"token": create_session(user.id)}

Real-World Breaches

  • Slack (2015) โ€” Token theft via XSS; session tokens were long-lived and not rotated, allowing persistent account access
  • Zoom (2020) โ€” Credential stuffing attack used 500,000 stolen credentials; valid pairs were sold on dark web forums
  • GitHub (2012) โ€” Mass password reset after credential stuffing; GitHub accounts were protected only by password with no MFA requirement
  • Colonial Pipeline (2021) โ€” VPN account with no MFA was compromised via a leaked password from a breached database; led to the largest US fuel pipeline shutdown

How to Fix โ€” Checklist

  • Cryptographically random session tokens โ€” secrets.token_hex(32); store all session data server-side
  • Uniform error messages โ€” always return the same message for invalid credentials regardless of which field was wrong
  • Account lockout / rate limiting โ€” lock after 5โ€“10 failed attempts; exponential backoff; CAPTCHA
  • Rotate session ID on login โ€” create a new token immediately after successful authentication; invalidate the pre-login token
  • Short session expiry โ€” idle timeout (15โ€“30 min for sensitive apps) + absolute timeout (8โ€“24 hours)
  • MFA for all privileged accounts โ€” TOTP (Google Authenticator, Authy) or hardware keys (FIDO2/WebAuthn) for admin and service accounts
  • Breach credential checks โ€” on registration and password change, check against HaveIBeenPwned Passwords API
Challenge 1

Weak Session Token โ€” MD5(username)

The session token is MD5(username) โ€” completely predictable. Compute the MD5 of admin and use it as the session cookie to access the admin dashboard.

Hint
echo -n admin | md5sum โ†’ 21232f297a57a5a743894a0e4a801fc3. Set cookie: w3b_session=21232f297a57a5a743894a0e4a801fc3
Challenge 2

Username Enumeration

The login endpoint returns different error messages for 'user not found' vs 'wrong password', allowing attackers to enumerate valid usernames.

Hint
Try username alice (exists) vs notauser (doesn't exist) and compare the error messages.