Secure Coding Checklist for Web Developers
The checklist of 45 modern web security (OWASP + NIST) recommendations

Secure Coding Checklist for Web Developers
A comprehensive baseline for web application security — covering OWASP Top 10, NIST guidelines, and modern full-stack practices (React, Next.js, Flask, Django).
⚡ Minimal Secure Baseline
Copy-paste starting point for every new project. Expand from here.
Passwords: Argon2id · min 12 chars · HaveIBeenPwned check
Cookies: httpOnly · Secure · SameSite=Lax
Headers: Content-Security-Policy (with nonces)
Strict-Transport-Security (HSTS)
Referrer-Policy: strict-origin
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
CSP baseline: default-src 'self';
script-src 'self' 'nonce-{random}';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
Auth: Session ID rotation on login
CSRF tokens (or SameSite=Strict + CORS allowlist)
API: Rate limiting — per IP, per user, per endpoint
Server-side validation on every request
Parameterized queries / ORM only
🔐 Authentication & Sessions
| # | Rule | Notes |
|---|---|---|
| 1 | Hash passwords with Argon2id (preferred) or bcrypt at minimum cost factor 12 | Argon2id won the Password Hashing Competition; better GPU-cracking resistance than bcrypt |
| 2 | Regenerate session IDs after login | Prevents session fixation attacks |
| 3 | Implement account lockout with progressive delays, not hard locks after N attempts | Hard lockouts enable DoS — use exponential backoff or CAPTCHA instead |
| 4 | Implement proper logout: invalidate server-side sessions, not just clear cookies | Client-side cookie clearing alone leaves server sessions alive |
| 5 | Check passwords against breached password lists (e.g. HaveIBeenPwned API) | Prioritize length (min 12 chars) over arbitrary complexity rules per NIST SP 800-63B |
| 6 | Enforce password policy server-side — prioritize minimum length (≥12 chars) and breached password checks rather than arbitrary complexity rules | NIST SP 800-63B explicitly discourages rules like "1 uppercase + 1 symbol + 1 number" — they create predictable patterns, not strong passwords |
| 7 | Use autocomplete="current-password" on login, autocomplete="new-password" on registration | Never use autocomplete="off" on password fields — it breaks password managers and encourages weak, memorable passwords (NIST guidance) |
🍪 Cookies & CSRF
| # | Rule | Notes |
|---|---|---|
| 8 | Never store authentication tokens or sensitive data in localStorage — prefer httpOnly cookies | httpOnly prevents JavaScript access to tokens; non-sensitive UI preferences are acceptable in localStorage |
| 9 | Set all three cookie flags: httpOnly, Secure, and SameSite | Use SameSite=Lax as the recommended default for most apps; use SameSite=Strict for maximum CSRF protection (note: Strict breaks OAuth redirects and any auth flow initiated from an external link) |
| 10 | Implement CSRF tokens on every state-changing form or request | For modern SPAs with Bearer token auth + strict CORS, tokens may be secondary; SameSite cookies are the primary CSRF defense |
🛡️ HTTP Headers & Transport
| # | Rule | Notes |
|---|---|---|
| 11 | Enforce HTTPS everywhere — redirect all HTTP to HTTPS at server level | Cover every endpoint, not just login pages |
| 12 | Add HSTS header: Strict-Transport-Security: max-age=31536000; includeSubDomains | Forces browsers to only connect via HTTPS in future requests; prevents downgrade attacks that server-side redirects alone cannot stop |
| 13 | Use a strong Content Security Policy (CSP) on every page, including object-src 'none' and base-uri 'self' | object-src 'none' blocks Flash/legacy plugin injection; base-uri 'self' prevents base tag hijacking. Baseline: default-src 'self'; script-src 'self' 'nonce-...'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; |
| 14 | Use nonces for every inline script in your CSP | Nonces make inline scripts safe without resorting to 'unsafe-inline' |
| 15 | Set frame-ancestors 'none' in CSP and X-Frame-Options: DENY as a fallback | frame-ancestors is the modern standard; X-Frame-Options covers older browsers — both serve the same clickjacking protection |
| 16 | Set Referrer-Policy: strict-origin | Prevents leaking full URLs to third-party domains |
| 17 | Set X-Content-Type-Options: nosniff | Prevents browsers from MIME-sniffing responses away from the declared content type — a simple one-liner with meaningful impact |
| 18 | Disable HTTP methods you don't use — specifically TRACE and TRACK at server level | Keep OPTIONS if using CORS; REST APIs will legitimately need PUT and DELETE |
| 19 | Never cache sensitive API responses: Cache-Control: no-store | Prevents proxies, CDNs, and browsers from storing private data |
| 20 | Validate Content-Type headers on every API request | Prevents MIME confusion and content-sniffing attacks |
💉 Input, Output & Injection
| # | Rule | Notes |
|---|---|---|
| 21 | Always use parameterized queries or ORMs — never concatenate user input into SQL | Primary defense against SQL injection (OWASP Top 10 #1) |
| 22 | Always re-validate server-side — never trust client-side validation alone | Client validation is UX; server validation is security |
| 23 | Use context-aware output encoding before rendering user data | React escapes text automatically, but treat dangerouslySetInnerHTML like eval() — avoid it entirely unless absolutely necessary |
| 24 | Validate and restrict outbound HTTP requests to prevent SSRF | Use allowlists for permitted destinations; explicitly block requests to 169.254.169.254 (cloud metadata), 10.x.x.x, 172.16.x.x, 192.168.x.x, and other internal IP ranges. Especially critical for cloud-hosted apps (OWASP Top 10 2021 #10) |
| 25 | Validate file upload types using magic numbers (file signature bytes), not extension or Content-Type header | A renamed .exe will still have a non-image file signature |
| 26 | Strip metadata from every user-uploaded file before storing | Images can contain GPS coordinates, device info, and other PII in EXIF data |
🔑 Tokens, Secrets & Cryptography
| # | Rule | Notes |
|---|---|---|
| 27 | Use constant-time string comparison for token validation | Prevents timing attacks that can leak token values bit-by-bit |
| 28 | Never use MD5 or SHA-1 for anything security-related | Both are cryptographically broken; use SHA-256 or better |
| 29 | Scope OAuth tokens to minimum required permissions only | Principle of least privilege — reduces blast radius on token compromise |
| 30 | Use short-lived presigned URLs for private file access — never public bucket URLs | Presigned URLs expire; public bucket URLs do not |
| 31 | Use subresource integrity (SRI) for every external script you load | Ensures CDN-served scripts haven't been tampered with |
🗄️ Data, Logging & Infrastructure
| # | Rule | Notes |
|---|---|---|
| 32 | Never log passwords, tokens, or PII — even accidentally | Audit log pipelines specifically; structured loggers can inadvertently capture request bodies |
| 33 | Never expose stack traces or error details in production responses | Return generic error messages to clients; log details server-side only |
| 34 | Use separate DB credentials per environment — never share production credentials | Dev/staging compromise should never cascade to production data |
| 35 | Disable directory listing on your server — never expose file structure | Directory listings reveal app internals and aid reconnaissance |
| 36 | Implement rate limiting at three levels: per IP, per authenticated user, and per endpoint — not just on login | Protects against scraping, credential stuffing, and volumetric DoS across all routes |
📦 Dependencies & Deployment
| # | Rule | Notes |
|---|---|---|
| 37 | Keep dependency list minimal — every extra package is an attack surface | Audit regularly; remove unused packages |
| 38 | Pin dependencies with lockfiles committed to source control | Prevents supply-chain attacks from silently updated transitive dependencies |
| 39 | Monitor for dependency vulnerabilities weekly with Snyk, npm audit, or pip-audit | Vulnerabilities in dependencies are discovered continuously |
| 40 | Scan Docker images for vulnerabilities before every deployment | Use docker scout, Trivy, or Snyk Container |
🔴 Advanced Security Rules — Senior-Level Checklist
These issues appear regularly in real-world security audits but are rarely covered in standard checklists. SSRF is already covered in rule #24 above.
41 · Never Trust the Host Header — Prevent Host Header Attacks
Attackers can manipulate the Host header to poison generated URLs in password reset emails, verification links, and redirects.
Example attack:
Host: attacker.com
Your backend might then generate:
https://attacker.com/reset?token=abc123
Defense: Always use a hardcoded canonical domain when generating URLs. Never derive URLs from request.host.
# Django — enforce an allowlist
ALLOWED_HOSTS = ["example.com", "www.example.com"]
# Flask — never use request.host for URL generation
# Use a hardcoded BASE_URL env variable instead
BASE_URL = os.environ.get("BASE_URL", "https://example.com")
42 · Restrict CORS Strictly — Never Combine Wildcard with Credentials
Never use this combination:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
This allows any website to make authenticated requests to your API using the victim's cookies.
Correct configuration:
For authenticated APIs — allowlist specific origins only:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
For fully public APIs with no authentication:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: false (or omit entirely)
43 · Prevent Cache Poisoning on Dynamic Responses
Dynamic responses that depend on authentication or user state must never be cached by shared caches or CDNs. If a proxy caches a personalized response, another user may receive it.
Vulnerable:
Cache-Control: public, max-age=3600
Secure — for authenticated responses:
Cache-Control: no-store
Secure — for semi-dynamic content:
Cache-Control: private, max-age=0
Also validate headers that influence caching behavior — attackers can inject via:
X-Forwarded-HostX-Forwarded-ProtoHost
44 · Prevent XS-Leaks — Cross-Site Information Leaks
XS-Leaks exploit subtle browser behaviors (timing differences, iframe loading states, redirect detection) to infer private user state across origins — such as whether a user is logged in, whether a resource exists, or the content of a private response.
Mitigation — add cross-origin isolation headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Combined with SameSite cookies, a strict CSP, and frame-ancestors, these headers form a robust isolation boundary.
45 · Validate Redirect Targets — Prevent Open Redirects
Never allow unvalidated redirect URLs from user input. The classic vector:
/login?next=https://evil.com
→ 302 Location: https://evil.com
Attackers abuse open redirects for phishing, OAuth token theft, and bypassing security filters.
Safe redirect logic — allow only relative paths or explicitly whitelisted domains:
from urllib.parse import urlparse
def safe_redirect(url, fallback="/"):
parsed = urlparse(url)
# Allow only relative paths (no scheme, no netloc)
if not parsed.scheme and not parsed.netloc:
return redirect(url)
return redirect(fallback)
Never redirect to any URL containing a scheme (http://, https://) unless it is in a hardcoded allowlist.
✅ Quick Reference: Key Upgrades from Common Mistakes
| ❌ Common Mistake | ✅ Correct Practice |
|---|---|
localStorage for JWT tokens | httpOnly; Secure; SameSite=Lax cookie |
autocomplete="off" on passwords | autocomplete="current-password" / "new-password" |
| bcrypt cost 10 | Argon2id (or bcrypt cost ≥ 12) |
| Hard lockout after 5 attempts | Progressive delays + CAPTCHA |
| MD5/SHA-1 for hashing | SHA-256+ or Argon2id for passwords |
| Arbitrary complexity rules (1 special char, 1 number) | Minimum 12 chars + HaveIBeenPwned check |
X-Frame-Options alone | X-Frame-Options + CSP frame-ancestors 'none' |
| HTTPS redirect only | HTTPS redirect + HSTS header |
| Trust file extension on upload | Validate magic numbers (file signature) |
dangerouslySetInnerHTML with user data | Context-aware encoding; avoid entirely |
| Raw SQL string concatenation | Parameterized queries / ORM |
| No outbound request validation | SSRF allowlist + block internal IP ranges |
| Missing MIME type enforcement | X-Content-Type-Options: nosniff |
SameSite=Strict everywhere | SameSite=Lax default; Strict only where OAuth flows aren't used |
Rate limiting only on /login | Per-IP + per-user + per-endpoint limits across all routes |
Trusting request.host for URL generation | Hardcoded BASE_URL env variable |
CORS: * + credentials: true | Explicit origin allowlist with credentials |
Cache-Control: public on auth responses | Cache-Control: no-store |
| No cross-origin isolation headers | COOP + CORP + COEP headers |
Unvalidated ?next= redirect params | Relative-path-only or domain allowlist redirect logic |
Sources: OWASP Top 10 (2021), NIST SP 800-63B, Password Hashing Competition, CWE/SANS Top 25, PortSwigger Web Security Academy