9 min read

Intermediate

Authentication Remediation

Risk Severity
Critical
Fix Effort
Medium
Est. Time
2-4 hours
Reference
A07:2025 CWE-287

Broken authentication vulnerabilities allow attackers to compromise passwords, keys, or session tokens, or to exploit other implementation flaws to assume other users' identities.

Never Roll Your Own Crypto

Authentication is one of the easiest things to get wrong. Always use battle-tested libraries and frameworks rather than implementing your own password hashing or session management. A single oversight can compromise all user accounts.

Authentication Security Flow

Secure Password Storage

Passwords must never be stored in plain text. Use strong, slow hashing algorithms like Argon2, bcrypt, or scrypt.

Argon2 ⭐

Winner of Password Hashing Competition (2015). Memory-hard, resistant to GPU/ASIC attacks. Recommended for new projects.

bcrypt

Industry standard since 1999. Well-tested, widely supported. Work factor (cost) adjustable. Great choice for most applications.

scrypt

Memory-hard like Argon2. Good for high-security applications. More complex to configure correctly.

❌ Never Use These for Passwords

MD5, SHA-1, SHA-256 — These are fast hashes designed for integrity checking, not password storage. GPUs can compute billions of SHA-256 hashes per second, making brute-force attacks trivial.

Python Implementation

Using bcrypt:

import bcrypt

def hash_password(password: str) -> bytes:
    # Generate salt and hash (work factor 12)
    return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt(12))

def verify_password(password: str, hashed: bytes) -> bool:
    return bcrypt.checkpw(password.encode('utf-8'), hashed)

Using argon2 (Preferred for new applications):

from argon2 import PasswordHasher

ph = PasswordHasher()

def hash_password(password: str) -> str:
    return ph.hash(password)

def verify_password(password: str, hashed: str) -> bool:
    try:
        return ph.verify(hashed, password)
    except:
        return False

Node.js Implementation

Using bcrypt:

const bcrypt = require('bcrypt');
const SALT_ROUNDS = 12;

async function hashPassword(password) {
    return await bcrypt.hash(password, SALT_ROUNDS);
}

async function verifyPassword(password, hash) {
    return await bcrypt.compare(password, hash);
}

PHP Implementation

Using password_hash (uses bcrypt by default):

// Hash password
$hash = password_hash($password, PASSWORD_DEFAULT);

// Verify password
if (password_verify($password, $hash)) {
    // Password correct
}

// For Argon2 (PHP 7.2+)
$hash = password_hash($password, PASSWORD_ARGON2ID);

Session Management

Sessions must be protected against hijacking and fixation attacks. Ensure cookies are secure and sessions are properly invalidated.

🍪 Cookie Security Flags Explained

Secure

Cookie only sent over HTTPS. Prevents interception on insecure networks.

HttpOnly

Cookie inaccessible to JavaScript. Prevents XSS from stealing session tokens.

SameSite

Controls cross-site cookie sending. Lax prevents CSRF on POST requests.

Max-Age / Expires

Session timeout. Shorter = more secure but less convenient. 1 hour is a good balance.

Flask (Python)

from flask import Flask, session
from datetime import timedelta

app = Flask(__name__)
app.config.update(
    SECRET_KEY='your-secure-secret-key-here',
    SESSION_COOKIE_SECURE=True,       # HTTPS only
    SESSION_COOKIE_HTTPONLY=True,     # No JavaScript access
    SESSION_COOKIE_SAMESITE='Lax',    # CSRF protection
    PERMANENT_SESSION_LIFETIME=timedelta(hours=1),
    SESSION_REFRESH_EACH_REQUEST=True
)

Express.js (Node.js)

const session = require('express-session');
const RedisStore = require('connect-redis')(session);

app.use(session({
    store: new RedisStore({ client: redisClient }),
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: {
        secure: true,          // HTTPS only
        httpOnly: true,        // No JavaScript access
        sameSite: 'lax',       // CSRF protection
        maxAge: 3600000        // 1 hour
    }
}));

PHP

// Session configuration
ini_set('session.cookie_secure', 1);     // HTTPS only
ini_set('session.cookie_httponly', 1);   // No JavaScript
ini_set('session.cookie_samesite', 'Lax');
ini_set('session.use_strict_mode', 1);
ini_set('session.gc_maxlifetime', 3600); // 1 hour

session_start();

// Regenerate session ID after login
session_regenerate_id(true);

Authentication Hardening Checklist

  • ✅ Use bcrypt, Argon2, or scrypt for password hashing
  • ✅ Enforce minimum password length (12+ characters)
  • ✅ Check passwords against known breached password lists
  • ✅ Implement account lockout after failed attempts
  • ✅ Use MFA for sensitive operations
  • ✅ Regenerate session IDs after authentication
  • ✅ Set secure cookie flags (Secure, HttpOnly, SameSite)
  • ✅ Implement proper logout (invalidate server-side session)
  • ✅ Use rate limiting on authentication endpoints

Testing Verification

Use these tests to verify your authentication implementation is secure.

Password Storage Tests

# Check if passwords are hashed (not plain text)

Database query should return hash starting with $2b$ (bcrypt), $argon2 (Argon2)

# Verify timing attack resistance

Response time should be constant regardless of username validity

# Test account enumeration

Error messages should be generic: "Invalid credentials" (not "User not found")

Session Security Tests

# Check cookie flags with curl
curl -I https://yoursite.com/login -c - | grep -i "set-cookie"

# Expected flags in cookie:
# Secure; HttpOnly; SameSite=Lax

# Test session fixation
1. Note session ID before login
2. Login successfully  
3. Verify session ID changed after login

Rate Limiting Tests

# Test brute force protection with hydra (test environment only!)
hydra -l admin -P /usr/share/wordlists/rockyou.txt \
  http-post-form "//login:username=^USER^&password=^PASS^:Invalid"

# Expected: Account lockout after 5-10 failed attempts
# Expected: 429 Too Many Requests response

Common Mistakes

❌ Weak Password Hashing

# DON'T: Using MD5/SHA for passwords
hash = hashlib.md5(password).hexdigest()
hash = hashlib.sha256(password).hexdigest()

Fast hashes allow billions of guesses per second

✅ Correct Approach

# DO: Use purpose-built password hashing
from argon2 import PasswordHasher
ph = PasswordHasher()
hash = ph.hash(password)

Memory-hard algorithms resist GPU attacks

❌ User Enumeration

# DON'T: Different messages reveal info
if not user_exists(email):
    return "User not found"
if not password_correct:
    return "Wrong password"

Attackers can discover valid usernames

✅ Correct Approach

# DO: Generic error message
if not authenticate(email, password):
    return "Invalid credentials"
# Same message whether user exists or not

No information leakage to attackers

❌ Missing Session Regeneration

# DON'T: Keep same session ID after login
session['user_id'] = user.id
# Session ID unchanged = session fixation!

Allows session fixation attacks

✅ Correct Approach

# DO: Regenerate session after authentication
session.clear()
session.regenerate()  # New session ID
session['user_id'] = user.id

Prevents session fixation attacks

Authentication Security Labs

Practice identifying and fixing authentication vulnerabilities.

Broken Authentication PortSwigger medium
Password brute-forceSession fixationMulti-factor bypass
DVWA Brute Force DVWA easy
Dictionary attacksCredential stuffingAccount lockout bypass

Evidence to Capture

Hash construction: The stored value format — $argon2id$v=19$m=65536, $2b$12$ or a bare 64-character hex digest — plus two records proving the salt differs per user. Never paste a real password.

Session before and after login: The cookie value issued pre-authentication and the value issued after a successful login. Identical values are the fixation finding, and the pair is the proof.

Login response pair: The 200 and the 401 bodies side by side. Identical wording for unknown user and wrong password is the anti-enumeration control working; a difference is the finding.

Reset and MFA flows: The reset token, its length and expiry, and whether it survives a second use; for MFA, the request that reaches a protected page after completing only the first factor.

Rate limits: The number of failed attempts accepted per account and per source address before the response changes, with the timestamps that show the window.

Likely False Positives

  • Millisecond timing difference: A missing row is faster than a hash comparison, but only measurably so over thirty or more samples. Below that the difference is network and scheduler noise, not user enumeration.
  • Lockout per source only: Failing from one address says nothing about the control if it is keyed on account or on a distributed counter. Retest from a second address and at low rate.
  • Session value that changes every request: Framework regeneration or a load balancer cookie is not fixation. Fixation requires that a value supplied by the attacker was accepted as the session.
  • Non-session cookie missing HttpOnly: Consent, locale and CSRF cookies carry different sensitivity. Scope the finding to the cookie that authenticates.
  • Strong hash labelled weak: A bcrypt cost of 10 or 12 is not weak because the algorithm is old. Compare the measured crack time for that cost against the password policy, not the algorithm name.

Testing Checklist

Discovery

  • ☐ List every authentication entry point: login, password reset, remember-me, API keys, basic auth, and each OAuth or SAML callback.
  • ☐ Note the session cookie name, its flags, and where the application validates the session rather than only reading it.
  • ☐ Map the privilege change points that should force a new session: login, password change, role escalation, MFA completion.

Confirmation

  • ☐ Attempt credential stuffing at a sensible rate and record when, if ever, the lockout or throttle appears.
  • ☐ Test reset token entropy, expiry, reuse and whether it is bound to the account it was issued for.
  • ☐ Set a known session value before login and check whether it survives as the authenticated session.
  • ☐ Skip the second factor by calling the post-login endpoint directly and by replaying the first-factor response.

Impact

  • ☐ Benchmark the observed hash offline in the matching cracker mode and state the practical time for one GPU, per password length in policy.
  • ☐ Establish what one compromised account reaches: admin functions, other tenants, or a token that grants API scope.
  • ☐ Check whether the reset flow alone is enough to take an account over, since that needs no password crack at all.

Remediation

Structural fix

  • • Hash with Argon2id, or bcrypt at cost 12 or above, through a maintained library. Per-user salt is generated by the library, never stored beside the hash by hand.
  • • Rotate the session identifier on login and on every privilege change, and invalidate the old one server-side rather than only clearing the cookie.
  • • Enforce MFA on the server for each protected request, not by redirecting the browser to the second-factor page.
  • • Issue reset tokens from a CSPRNG, store only their hash, expire within minutes, and consume on first use (UPDATE ... WHERE token_hash = ? AND used_at IS NULL).

Compensating control

  • • Rate limit and lock progressively, keyed on both account and source, with the counter held outside the request process.
  • • Return one generic message for every failure path and make the response time for unknown users match the known-user path.
  • • Screen new passwords against a breached-password corpus, and alert on spraying patterns across many accounts.

CWE References: CWE-287