Web Security Cheatsheet
Authentication and Sessions
Use this Web Security reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Password storage
Never store a password recoverably, and never hash it with a fast algorithm. SHA-256 and MD5 are designed to be fast, which is exactly wrong here. Use a slow, memory-hard, salted password hash. These are the current OWASP Password Storage recommendations, with real parameters.
| Algorithm | Minimum parameters | When |
|---|---|---|
| Argon2id | 19 MiB memory, iterations t=2, parallelism p=1 | First choice |
| scrypt | N=2^17, r=8 (1024-byte blocks), p=1 | When Argon2id is unavailable |
| bcrypt | work factor ≥ 10, input capped at 72 bytes | Legacy systems |
| PBKDF2 | 600,000 iterations, HMAC-SHA-256 | When FIPS-140 compliance is required |
# Python, argon2-cffi from argon2 import PasswordHasher ph = PasswordHasher(memory_cost=19456, time_cost=2, parallelism=1) stored = ph.hash(password) # salt is generated and embedded try: ph.verify(stored, candidate) if ph.check_needs_rehash(stored): stored = ph.hash(candidate) # transparent upgrade on login except VerifyMismatchError: ...
// Node, argon2 import argon2 from 'argon2' const hash = await argon2.hash(pw, { type: argon2.argon2id, memoryCost: 19456, timeCost: 2, parallelism: 1, }) const ok = await argon2.verify(hash, candidate)
// Java, Spring Security PasswordEncoder enc = new Argon2PasswordEncoder(16, 32, 1, 19456, 2); String hash = enc.encode(raw); boolean ok = enc.matches(raw, hash);
Do not add your own salt, every one of these generates and embeds a unique salt. Never truncate the password. bcrypt silently ignores bytes past 72, so a long passphrase gets cut, and pre-hashing to work around that has its own pitfalls.
Peppering and rehashing
A pepper is a secret shared across all hashes, kept outside the database, in a KMS or an env secret. It buys nothing if the attacker gets both, but it means a database-only dump is not crackable. Apply it as an HMAC before the password hash, or use a hash implementation with native secret-key support.
import hmac, hashlib pre = hmac.new(PEPPER, password.encode(), hashlib.sha256).hexdigest() stored = ph.hash(pre) # pepper is separate from the DB, rotatable
Rehash on login is how you migrate parameters. Verify with the stored parameters, and if they are below your current policy, recompute from the plaintext you already have in hand. Never bulk-rehash a stored hash by wrapping it, that locks you into the old algorithm forever.
Session lifecycle
| Rule | Why |
|---|---|
| Generate the id from a CSPRNG, 128 bits or more | Guessing must be infeasible |
| Regenerate the id on login | Prevents session fixation |
| Regenerate on privilege change | A step up to admin must not reuse the old id |
| Invalidate server side on logout | Deleting the cookie alone leaves the session valid |
| Absolute timeout, hours not weeks | Bounds the value of a stolen session |
| Idle timeout | Catches abandoned sessions on shared machines |
| Bind nothing to IP or User-Agent as the only check | Mobile networks change IPs, it breaks real users |
| Offer "log out all devices" | The only recovery a user has after a theft |
Session fixation is the attack these prevent. If the server accepts a session id supplied by the visitor and keeps it across authentication, whoever chose that id becomes logged in as the user.
# Vulnerable: the pre-login session id survives authentication session['user_id'] = user.id # Fixed: new id, old session destroyed session.clear() session.regenerate() # Flask-Login: login_user() does this for you session['user_id'] = user.id
// PHP: the canonical fix, one line session_regenerate_id(true); // true destroys the old session file
Never accept a session id from a URL or a request parameter, only from a cookie.
MFA and credential policy
MFA is the single highest-impact control against credential stuffing and phishing, because it breaks the value of a leaked password.
| Factor | Phishing resistant | Notes |
|---|---|---|
| WebAuthn / passkeys | Yes | Origin-bound by the browser, the strongest option |
| Hardware security key (FIDO2) | Yes | Same mechanism, separate device |
| TOTP (RFC 6238 authenticator app) | No | Good, but a real-time relay can capture a code |
| Push approval | No | Vulnerable to prompt fatigue, add number matching |
| SMS or email codes | No | Weakest, exposed to SIM swap. A fallback, not a factor |
Password policy per current NIST SP 800-63B guidance:
- Minimum 8 characters, allow at least 64, and allow every printable character including spaces and Unicode. - No composition rules, no forced mixture of character classes. They push users toward predictable patterns. - No mandatory periodic rotation. Force a change only on evidence of compromise. - Screen new passwords against a breached-password list. This does more than every complexity rule combined. - Support paste, so password managers work.
Login and recovery flows
The flows around login leak more than login itself.
| Flaw | Fix |
|---|---|
| Different response for unknown user vs wrong password | One generic message, and constant-time behavior on both paths |
| Timing difference on unknown user | Hash a dummy password anyway so both paths cost the same |
| Unlimited attempts | Rate limit per account and per IP, with backoff |
| Predictable reset token | CSPRNG, ≥ 128 bits, single use, short expiry (15-60 minutes) |
| Reset token in the URL logged by a proxy | Acceptable if single use and short lived, and never in a redirect target |
| Reset does not invalidate sessions | Revoke all sessions on password change |
| Host header used to build the reset link | Use a configured base URL, never Host from the request |
| User enumeration in signup or reset | Same response whether the account exists or not |
| No notification | Email the user on password, email, and MFA changes |
| MFA skipped on the reset path | Recovery must be at least as strong as login |
# Constant-ish response regardless of whether the user exists user = User.query.filter_by(email=email).first() if user is None: ph.verify(DUMMY_HASH, password_guess_ignored) # burn the same time return generic_login_error()
Tokens and JWT pitfalls
If you use JWTs for sessions, these are the recurring failures.
| Pitfall | Fix |
|---|---|
Trusting the header's alg | Pin the expected algorithm server side, reject none |
| Confusing HMAC and RSA verification | Never let a public key be used as an HMAC secret |
| No expiry check | Validate exp, nbf, iss, and aud explicitly |
| Weak HMAC secret | 256 bits from a CSPRNG, not a passphrase |
| No revocation | Keep access tokens short (minutes) with a revocable refresh token |
| Secrets in the payload | The payload is base64, not encrypted. Anyone can read it. |
| Decoding without verifying | decode is not verify, check the library's API |
import jwt claims = jwt.decode(token, KEY, algorithms=["HS256"], # pinned, a list audience="api.example.com", issuer="https://id.example.com", options={"require": ["exp", "iat", "aud", "iss"]})
Recognizing it in review
grep -rniE "md5\(|sha1\(|sha256\(.*password|hashlib\.sha" src/ grep -rn "localStorage.setItem.*token\|sessionStorage.setItem.*token" src/ grep -rniE "algorithms=\[.*none|verify=False|verify_signature.*False" src/ grep -rn "httpOnly: *false\|secure: *false" src/
Checklist:
- Passwords use Argon2id, scrypt, bcrypt, or PBKDF2 at the parameters above, and
the code rehashes on login when the policy tightens.
- The session id is regenerated on login and on privilege change, and destroyed
server side on logout.
- Cookies carry HttpOnly, Secure, SameSite, and no Domain.
- Login, signup, and reset return identical responses for existing and unknown
accounts.
- Reset tokens are CSPRNG, single use, and short lived, and a reset revokes every
session.
- Rate limiting exists per account and per IP with backoff.
- MFA is available, and WebAuthn is offered where it can be.
- Every JWT verification pins the algorithm and validates exp, aud, and iss.
- No credential, token, or secret appears in a log line or a URL.
Further reading: the OWASP Authentication Cheat Sheet, Password Storage Cheat Sheet, and Session Management Cheat Sheet.