Web Security Cheat Sheet

Comprehensive defensive security reference for web developers. Covers OWASP Top 10, injection prevention, headers, authentication, and more.

1. OWASP Top 10 (2021)

#VulnerabilityDescriptionPrevention
A01Broken Access ControlUsers act outside intended permissionsDeny by default, RBAC, server-side checks
A02Cryptographic FailuresWeak or missing encryption of sensitive dataTLS everywhere, strong algorithms, no hardcoded keys
A03InjectionSQL, NoSQL, OS, LDAP injection via untrusted dataParameterized queries, input validation, ORM
A04Insecure DesignMissing or ineffective security controls by designThreat modeling, secure design patterns
A05Security MisconfigurationDefault configs, open cloud storage, verbose errorsHardened defaults, automated config audits
A06Vulnerable ComponentsUsing libraries with known vulnerabilitiesnpm audit, Dependabot, SCA tools
A07Auth & ID FailuresBroken authentication or session managementMFA, rate limiting, secure session handling
A08Software & Data IntegrityUntrusted updates, CI/CD pipeline compromiseVerify signatures, SRI hashes, signed commits
A09Logging & Monitoring FailuresInsufficient logging hinders breach detectionLog security events, alerting, audit trails
A10SSRFServer fetches attacker-controlled URLsAllowlist URLs, block internal IPs, validate schemes

2. SQL Injection Prevention

Rule #1: Never concatenate user input into SQL. Always use parameterized queries.

LanguageParameterized Query Example
Node.js
// pg (PostgreSQL)
const result = await pool.query(
  'SELECT * FROM users WHERE email = $1', [email]
);
// mysql2
const [rows] = await conn.execute(
  'SELECT * FROM users WHERE id = ?', [userId]
);
Python
# psycopg2
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))
# SQLAlchemy ORM (safe by default)
user = session.query(User).filter_by(email=email).first()
Java
PreparedStatement ps = conn.prepareStatement(
  "SELECT * FROM users WHERE email = ?");
ps.setString(1, email);
ResultSet rs = ps.executeQuery();
PHP
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute(['email' => $email]);

3. XSS Prevention

TechniqueImplementation
Output Encoding
// Encode for HTML context
function escapeHtml(str) {
  return str.replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');
}
// Use textContent instead of innerHTML
el.textContent = userInput; // Safe
el.innerHTML = userInput;   // DANGEROUS
Content Security Policy
Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self';
  frame-ancestors 'none';
DOMPurify
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(dirtyHtml);
// With config
const clean = DOMPurify.sanitize(dirty, {
  ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
  ALLOWED_ATTR: ['href']
});
Template EnginesReact: auto-escapes by default (avoid dangerouslySetInnerHTML)
Jinja2: auto-escapes by default ({{ var }})
EJS: use <%= var %> (escaped) not <%- var %> (raw)

4. CSRF Protection

MethodImplementation
CSRF Tokens
<!-- Server generates unique token per session -->
<form method="POST" action="/transfer">
  <input type="hidden" name="_csrf" value="a1b2c3...">
  ...
</form>
// Express: csurf middleware (or csrf-csrf)
app.use(csrfProtection);
res.render('form', { csrfToken: req.csrfToken() });
SameSite Cookies
Set-Cookie: session=abc123;
  SameSite=Strict;  // Best: no cross-site sending
  Secure;           // HTTPS only
  HttpOnly;         // No JS access
  Path=/;
  Max-Age=3600
Double-Submit Cookie
// 1. Set random value in cookie AND form field
// 2. Server verifies cookie value matches form value
// Works because attacker can't read cookies
res.cookie('csrf', token, { sameSite: 'strict' });
// Client sends token in header:
fetch('/api', { headers: { 'X-CSRF-Token': token } });

5. Security Headers

HeaderRecommended ValuePurpose
Content-Security-Policydefault-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; frame-ancestors 'none'Controls resource loading, prevents XSS
Strict-Transport-Securitymax-age=63072000; includeSubDomains; preloadForces HTTPS for 2 years
X-Frame-OptionsDENYPrevents clickjacking
X-Content-Type-OptionsnosniffPrevents MIME-type sniffing
Referrer-Policystrict-origin-when-cross-originControls referrer info leaking
Permissions-Policycamera=(), microphone=(), geolocation=()Disables browser APIs
X-XSS-Protection0Disable (deprecated, CSP is better)
Cross-Origin-Opener-Policysame-originIsolates browsing context
Cross-Origin-Resource-Policysame-originPrevents cross-origin reads

Nginx example:

add_header Content-Security-Policy "default-src 'self'; script-src 'self'" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

6. Authentication Best Practices

TopicImplementation
Password Hashing
// Node.js - bcrypt
const hash = await bcrypt.hash(password, 12); // cost=12
const valid = await bcrypt.compare(password, hash);

# Python - argon2 (preferred)
from argon2 import PasswordHasher
ph = PasswordHasher()
hash = ph.hash(password)
ph.verify(hash, password)
Never use: MD5, SHA-1, SHA-256 alone (too fast for passwords)
JWT Best Practices
  • Use RS256 or ES256 (asymmetric) in production
  • Set short expiry: exp = 15 min for access tokens
  • Use refresh tokens (long-lived, stored securely)
  • Always validate iss, aud, exp claims
  • Never store secrets in JWT payload (it is base64, not encrypted)
  • Reject "alg": "none" explicitly
Session Management
  • Regenerate session ID after login
  • Set HttpOnly, Secure, SameSite on cookies
  • Implement idle timeout (15-30 min) and absolute timeout (8-24 hrs)
  • Invalidate session server-side on logout

7. CORS Configuration

HeaderDescription
Access-Control-Allow-OriginSpecifies allowed origin. Use exact origin, never * with credentials.
Access-Control-Allow-MethodsGET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-HeadersContent-Type, Authorization
Access-Control-Allow-Credentialstrue (only with specific origin, not *)
Access-Control-Max-Age86400 (cache preflight for 24 hours)

Express CORS setup:

const cors = require('cors');
app.use(cors({
  origin: ['https://myapp.com', 'https://admin.myapp.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  credentials: true,
  maxAge: 86400
}));

Common mistakes: Access-Control-Allow-Origin: * with credentials: true (rejected by browsers). Reflecting the Origin header without validation (allows any site).

8. Input Validation

StrategyDetails
Allowlist (preferred)Define exactly what IS allowed. Reject everything else.
if (!/^[a-zA-Z0-9_-]{3,30}$/.test(username)) reject();
Denylist (weak)Block known-bad patterns. Easy to bypass with encoding tricks. Use only as defense-in-depth.
Type Coercionconst id = parseInt(req.params.id, 10);
if (isNaN(id) || id < 1) return res.status(400);
Schema Validation
// Zod (TypeScript)
const UserSchema = z.object({
  email: z.string().email().max(254),
  age: z.number().int().min(13).max(150),
  role: z.enum(['user', 'admin'])
});
const user = UserSchema.parse(req.body);
File Uploads
  • Validate MIME type AND file extension
  • Check magic bytes (file signature)
  • Rename uploaded files (never use original name)
  • Store outside webroot, serve via handler
  • Set max file size server-side

9. HTTPS & TLS

TopicBest Practice
TLS VersionMinimum TLS 1.2. Prefer TLS 1.3. Disable SSLv3, TLS 1.0, TLS 1.1.
Cipher SuitesUse AEAD ciphers: TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256
CertificatesUse Let's Encrypt (free, auto-renewable). Set up auto-renewal with certbot or ACME client. Monitor expiry.
HSTS Preload1. Add HSTS header with preload directive
2. Submit to hstspreload.org
3. Ensure ALL subdomains support HTTPS first
Redirect HTTP
# Nginx: redirect all HTTP to HTTPS
server {
  listen 80;
  return 301 https://$host$request_uri;
}

10. Secrets Management

MethodImplementation
Environment Variables
# .env file (NEVER commit this)
DATABASE_URL=postgres://user:pass@host:5432/db
JWT_SECRET=super-secret-key-here
API_KEY=sk_live_abc123

// Load in Node.js
import 'dotenv/config';
const dbUrl = process.env.DATABASE_URL;
.gitignore Rules
# Always ignore these
.env
.env.*
*.pem
*.key
credentials.json
service-account.json
Vault / KMSUse HashiCorp Vault, AWS Secrets Manager, or GCP Secret Manager for production. Rotate secrets regularly. Audit access.
Pre-commit HooksUse git-secrets or trufflehog to scan for leaked secrets before commits.

11. Rate Limiting

FrameworkImplementation
Express
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 minutes
  max: 100,                   // 100 requests per window
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many requests' }
});
app.use('/api/', limiter);
// Stricter for auth endpoints
app.use('/api/login', rateLimit({ windowMs: 900000, max: 5 }));
Django
# settings.py (Django REST Framework)
REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle',
    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '50/hour',
        'user': '1000/day',
    }
}
Nginx
http {
  limit_req_zone $binary_remote_addr
    zone=api:10m rate=10r/s;
}
server {
  location /api/ {
    limit_req zone=api burst=20 nodelay;
  }
}

12. Common Vulnerability Patterns

VulnerabilityAttackPrevention
Directory TraversalGET /files?name=../../etc/passwdResolve path, verify it starts with allowed base dir:
path.resolve(base, file).startsWith(base)
SSRFServer fetches http://169.254.169.254/ (cloud metadata)Allowlist domains/IPs. Block private ranges (10.x, 172.16.x, 169.254.x). Validate URL scheme.
Open Redirect/login?redirect=https://evil.comAllowlist redirect destinations. Use relative paths only. Validate URL starts with / (not //).
Mass AssignmentPOST { "role": "admin", "name": "Bob" }Explicitly pick allowed fields:
const { name, email } = req.body;
Never pass raw body to ORM.
Insecure DeserializationTampered serialized objects execute codeNever deserialize untrusted data. Use JSON (not native serialization). Validate schema after parsing.
ClickjackingTransparent iframe overlay tricks clicksX-Frame-Options: DENY and CSP frame-ancestors 'none'

Quick Security Checklist

Before Deploy
  • Run npm audit / pip audit
  • Scan with SAST tool (Semgrep, SonarQube)
  • Check for hardcoded secrets
  • Verify all security headers set
  • Test auth flows and permissions
Ongoing
  • Monitor dependency vulnerabilities
  • Review access logs for anomalies
  • Rotate secrets and API keys
  • Keep frameworks and OS updated
  • Run periodic penetration tests
Incident Response
  • Have a documented response plan
  • Rotate all compromised credentials
  • Preserve logs for forensics
  • Notify affected users promptly
  • Conduct post-mortem review