securityvibecoding

Secure Coding Checklist for Web Developers

The checklist of 45 modern web security (OWASP + NIST) recommendations

March 11, 202611 min read
Secure Coding Checklist for Web Developers

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

#RuleNotes
1Hash passwords with Argon2id (preferred) or bcrypt at minimum cost factor 12Argon2id won the Password Hashing Competition; better GPU-cracking resistance than bcrypt
2Regenerate session IDs after loginPrevents session fixation attacks
3Implement account lockout with progressive delays, not hard locks after N attemptsHard lockouts enable DoS — use exponential backoff or CAPTCHA instead
4Implement proper logout: invalidate server-side sessions, not just clear cookiesClient-side cookie clearing alone leaves server sessions alive
5Check passwords against breached password lists (e.g. HaveIBeenPwned API)Prioritize length (min 12 chars) over arbitrary complexity rules per NIST SP 800-63B
6Enforce password policy server-side — prioritize minimum length (≥12 chars) and breached password checks rather than arbitrary complexity rulesNIST SP 800-63B explicitly discourages rules like "1 uppercase + 1 symbol + 1 number" — they create predictable patterns, not strong passwords
7Use autocomplete="current-password" on login, autocomplete="new-password" on registrationNever use autocomplete="off" on password fields — it breaks password managers and encourages weak, memorable passwords (NIST guidance)

🍪 Cookies & CSRF

#RuleNotes
8Never store authentication tokens or sensitive data in localStorage — prefer httpOnly cookieshttpOnly prevents JavaScript access to tokens; non-sensitive UI preferences are acceptable in localStorage
9Set all three cookie flags: httpOnly, Secure, and SameSiteUse 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)
10Implement CSRF tokens on every state-changing form or requestFor modern SPAs with Bearer token auth + strict CORS, tokens may be secondary; SameSite cookies are the primary CSRF defense

🛡️ HTTP Headers & Transport

#RuleNotes
11Enforce HTTPS everywhere — redirect all HTTP to HTTPS at server levelCover every endpoint, not just login pages
12Add HSTS header: Strict-Transport-Security: max-age=31536000; includeSubDomainsForces browsers to only connect via HTTPS in future requests; prevents downgrade attacks that server-side redirects alone cannot stop
13Use 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';
14Use nonces for every inline script in your CSPNonces make inline scripts safe without resorting to 'unsafe-inline'
15Set frame-ancestors 'none' in CSP and X-Frame-Options: DENY as a fallbackframe-ancestors is the modern standard; X-Frame-Options covers older browsers — both serve the same clickjacking protection
16Set Referrer-Policy: strict-originPrevents leaking full URLs to third-party domains
17Set X-Content-Type-Options: nosniffPrevents browsers from MIME-sniffing responses away from the declared content type — a simple one-liner with meaningful impact
18Disable HTTP methods you don't use — specifically TRACE and TRACK at server levelKeep OPTIONS if using CORS; REST APIs will legitimately need PUT and DELETE
19Never cache sensitive API responses: Cache-Control: no-storePrevents proxies, CDNs, and browsers from storing private data
20Validate Content-Type headers on every API requestPrevents MIME confusion and content-sniffing attacks

💉 Input, Output & Injection

#RuleNotes
21Always use parameterized queries or ORMs — never concatenate user input into SQLPrimary defense against SQL injection (OWASP Top 10 #1)
22Always re-validate server-side — never trust client-side validation aloneClient validation is UX; server validation is security
23Use context-aware output encoding before rendering user dataReact escapes text automatically, but treat dangerouslySetInnerHTML like eval() — avoid it entirely unless absolutely necessary
24Validate and restrict outbound HTTP requests to prevent SSRFUse 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)
25Validate file upload types using magic numbers (file signature bytes), not extension or Content-Type headerA renamed .exe will still have a non-image file signature
26Strip metadata from every user-uploaded file before storingImages can contain GPS coordinates, device info, and other PII in EXIF data

🔑 Tokens, Secrets & Cryptography

#RuleNotes
27Use constant-time string comparison for token validationPrevents timing attacks that can leak token values bit-by-bit
28Never use MD5 or SHA-1 for anything security-relatedBoth are cryptographically broken; use SHA-256 or better
29Scope OAuth tokens to minimum required permissions onlyPrinciple of least privilege — reduces blast radius on token compromise
30Use short-lived presigned URLs for private file access — never public bucket URLsPresigned URLs expire; public bucket URLs do not
31Use subresource integrity (SRI) for every external script you loadEnsures CDN-served scripts haven't been tampered with

🗄️ Data, Logging & Infrastructure

#RuleNotes
32Never log passwords, tokens, or PII — even accidentallyAudit log pipelines specifically; structured loggers can inadvertently capture request bodies
33Never expose stack traces or error details in production responsesReturn generic error messages to clients; log details server-side only
34Use separate DB credentials per environment — never share production credentialsDev/staging compromise should never cascade to production data
35Disable directory listing on your server — never expose file structureDirectory listings reveal app internals and aid reconnaissance
36Implement rate limiting at three levels: per IP, per authenticated user, and per endpoint — not just on loginProtects against scraping, credential stuffing, and volumetric DoS across all routes

📦 Dependencies & Deployment

#RuleNotes
37Keep dependency list minimal — every extra package is an attack surfaceAudit regularly; remove unused packages
38Pin dependencies with lockfiles committed to source controlPrevents supply-chain attacks from silently updated transitive dependencies
39Monitor for dependency vulnerabilities weekly with Snyk, npm audit, or pip-auditVulnerabilities in dependencies are discovered continuously
40Scan Docker images for vulnerabilities before every deploymentUse 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-Host
  • X-Forwarded-Proto
  • Host

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 tokenshttpOnly; Secure; SameSite=Lax cookie
autocomplete="off" on passwordsautocomplete="current-password" / "new-password"
bcrypt cost 10Argon2id (or bcrypt cost ≥ 12)
Hard lockout after 5 attemptsProgressive delays + CAPTCHA
MD5/SHA-1 for hashingSHA-256+ or Argon2id for passwords
Arbitrary complexity rules (1 special char, 1 number)Minimum 12 chars + HaveIBeenPwned check
X-Frame-Options aloneX-Frame-Options + CSP frame-ancestors 'none'
HTTPS redirect onlyHTTPS redirect + HSTS header
Trust file extension on uploadValidate magic numbers (file signature)
dangerouslySetInnerHTML with user dataContext-aware encoding; avoid entirely
Raw SQL string concatenationParameterized queries / ORM
No outbound request validationSSRF allowlist + block internal IP ranges
Missing MIME type enforcementX-Content-Type-Options: nosniff
SameSite=Strict everywhereSameSite=Lax default; Strict only where OAuth flows aren't used
Rate limiting only on /loginPer-IP + per-user + per-endpoint limits across all routes
Trusting request.host for URL generationHardcoded BASE_URL env variable
CORS: * + credentials: trueExplicit origin allowlist with credentials
Cache-Control: public on auth responsesCache-Control: no-store
No cross-origin isolation headersCOOP + CORP + COEP headers
Unvalidated ?next= redirect paramsRelative-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