Authentication Remediation
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
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 FalseNode.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
Cookie only sent over HTTPS. Prevents interception on insecure networks.
Cookie inaccessible to JavaScript. Prevents XSS from stealing session tokens.
Controls cross-site cookie sending. Lax prevents CSRF on POST requests.
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 loginRate 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 notNo 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.idPrevents session fixation attacks