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.

AlgorithmMinimum parametersWhen
Argon2id19 MiB memory, iterations t=2, parallelism p=1First choice
scryptN=2^17, r=8 (1024-byte blocks), p=1When Argon2id is unavailable
bcryptwork factor ≥ 10, input capped at 72 bytesLegacy systems
PBKDF2600,000 iterations, HMAC-SHA-256When 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

RuleWhy
Generate the id from a CSPRNG, 128 bits or moreGuessing must be infeasible
Regenerate the id on loginPrevents session fixation
Regenerate on privilege changeA step up to admin must not reuse the old id
Invalidate server side on logoutDeleting the cookie alone leaves the session valid
Absolute timeout, hours not weeksBounds the value of a stolen session
Idle timeoutCatches abandoned sessions on shared machines
Bind nothing to IP or User-Agent as the only checkMobile 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.

FactorPhishing resistantNotes
WebAuthn / passkeysYesOrigin-bound by the browser, the strongest option
Hardware security key (FIDO2)YesSame mechanism, separate device
TOTP (RFC 6238 authenticator app)NoGood, but a real-time relay can capture a code
Push approvalNoVulnerable to prompt fatigue, add number matching
SMS or email codesNoWeakest, 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.

FlawFix
Different response for unknown user vs wrong passwordOne generic message, and constant-time behavior on both paths
Timing difference on unknown userHash a dummy password anyway so both paths cost the same
Unlimited attemptsRate limit per account and per IP, with backoff
Predictable reset tokenCSPRNG, ≥ 128 bits, single use, short expiry (15-60 minutes)
Reset token in the URL logged by a proxyAcceptable if single use and short lived, and never in a redirect target
Reset does not invalidate sessionsRevoke all sessions on password change
Host header used to build the reset linkUse a configured base URL, never Host from the request
User enumeration in signup or resetSame response whether the account exists or not
No notificationEmail the user on password, email, and MFA changes
MFA skipped on the reset pathRecovery 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.

PitfallFix
Trusting the header's algPin the expected algorithm server side, reject none
Confusing HMAC and RSA verificationNever let a public key be used as an HMAC secret
No expiry checkValidate exp, nbf, iss, and aud explicitly
Weak HMAC secret256 bits from a CSPRNG, not a passphrase
No revocationKeep access tokens short (minutes) with a revocable refresh token
Secrets in the payloadThe payload is base64, not encrypted. Anyone can read it.
Decoding without verifyingdecode 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.