Weak logins and sloppy sessions hand over accounts. Store passwords correctly, resist credential stuffing, and manage sessions so a stolen cookie can’t roam free.
Why: passwords must never be stored in plaintext or with fast/unsalted hashes — a database leak then exposes every account — so use a slow, salted, purpose-built password hash (bcrypt, argon2, scrypt). When: hash on registration and verify on login; never log or store the raw password. Where: the slowness is deliberate: it makes offline cracking of a leaked database infeasible.
# SAFE — a slow, salted password hash (bcrypt shown; argon2 is also excellent):
import bcrypt
def register(password: str) -> bytes:
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()) # salt is automatic
def verify(password: str, stored_hash: bytes) -> bool:
return bcrypt.checkpw(password.encode(), stored_hash)
# NEVER: plaintext, MD5/SHA-1, or unsalted SHA-256. They crack in seconds.Why: attackers replay leaked username/password pairs (credential stuffing) and guess weak passwords at scale, so login must have rate limiting, lockouts, and multi-factor authentication. When: throttle by IP and account, add MFA for anything sensitive, and check passwords against known-breached lists. Where: MFA is the single highest-impact control — it defeats stuffing even when a password is correct.
Login hardening (defense in depth):
[ ] rate-limit login attempts per IP AND per account
[ ] exponential backoff / temporary lockout after N failures
[ ] multi-factor authentication (MFA) — the biggest single win
[ ] block known-breached passwords (Have I Been Pwned API)
[ ] generic error ("invalid email or password") — don't reveal which
MFA stops credential stuffing even when the password is valid.Why: after login the session token IS the user, so if it leaks or lives forever an attacker gets in — sessions need secure cookies, expiry, and rotation. When: issue a new session ID on login (prevent fixation), set HttpOnly/Secure/SameSite, and expire idle sessions. Where: HttpOnly stops JavaScript (and thus XSS) from stealing the cookie, tying this to the XSS lesson.
# Set the session cookie with protective flags:
response.set_cookie(
"session", token,
httponly=True, # JS can't read it -> XSS can't steal it
secure=True, # sent only over HTTPS
samesite="Lax", # not sent on cross-site requests -> mitigates CSRF
max_age=3600, # expires -> a stolen cookie doesn't last forever
)
# Also: issue a NEW session id on login (prevents session fixation),
# and invalidate the session server-side on logout.