Security Review
OWASP Top 10 (2021)
Use this as a baseline checklist when reviewing any application for security vulnerabilities.
A01: Broken Access Control
Users acting outside their intended permissions.
// VULNERABLE: No authorization check app.get('/api/users/:id', async (req, res) => { const user = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id]); res.json(user); });
// FIXED: Verify the requesting user has permission app.get('/api/users/:id', authenticate, async (req, res) => { if (req.user.id !== req.params.id && req.user.role !== 'admin') { return res.status(403).json({ error: 'Forbidden' }); } const user = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id]); res.json(user); });
A02: Cryptographic Failures
-
Use TLS 1.2+ for all data in transit. Hash passwords with bcrypt (cost 12+) or argon2.
-
Never use MD5 or SHA-1 for password hashing. Encrypt data at rest with AES-256-GCM.
A03: Injection
VULNERABLE: SQL injection via string concatenation
cursor.execute(f"SELECT * FROM users WHERE name = '{user_input}'")
FIXED: Parameterized query
cursor.execute("SELECT * FROM users WHERE name = %s", (user_input,))
A04: Insecure Design
-
Rate limit authentication endpoints. Use CAPTCHA for public forms.
-
Apply least privilege. Threat model before building, not after.
A05: Security Misconfiguration
-
Disable directory listing. Remove default credentials and demo accounts.
-
Disable verbose errors in production. Review cloud IAM policies.
A06: Vulnerable and Outdated Components
npm audit # Node.js pip-audit # Python bundle audit # Ruby
A07: Identification and Authentication Failures
-
Enforce strong password policies (length over complexity). Account lockout after repeated failures.
-
MFA for sensitive operations. Invalidate sessions on logout and password change.
A08: Software and Data Integrity Failures
- Verify dependency integrity (lock files, checksums). Sign releases. Protect CI/CD pipelines.
A09: Security Logging and Monitoring Failures
-
Log all authentication events, authorization failures, and input validation failures.
-
Ensure logs are tamper-proof and retained.
A10: Server-Side Request Forgery (SSRF)
Application fetches a URL provided by the user without validation.
// VULNERABLE: User controls the URL app.get('/fetch', async (req, res) => { const response = await fetch(req.query.url); res.send(await response.text()); });
// FIXED: Allowlist of permitted domains const ALLOWED_HOSTS = ['api.trusted-service.com']; app.get('/fetch', async (req, res) => { const url = new URL(req.query.url); if (!ALLOWED_HOSTS.includes(url.hostname)) { return res.status(400).json({ error: 'Domain not allowed' }); } res.send(await (await fetch(url.toString())).text()); });
Authentication Patterns
Password Hashing
// Node.js with bcrypt const bcrypt = require('bcrypt'); const SALT_ROUNDS = 12; const hash = await bcrypt.hash(password, SALT_ROUNDS); // Hash const isValid = await bcrypt.compare(candidatePassword, storedHash); // Verify
Python with argon2
from argon2 import PasswordHasher ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4) hash = ph.hash(password) # Hash ph.verify(hash, candidate_password) # Verify — raises VerifyMismatchError on failure
Session Management
Practice Description
Regenerate session ID on login Prevents session fixation attacks
Set HttpOnly flag on cookies Prevents JavaScript access to session cookie
Set Secure flag on cookies Ensures cookie sent only over HTTPS
Set SameSite=Strict or Lax
Prevents CSRF via cookie
Expire sessions after inactivity Limit window of exposure for stolen sessions
Invalidate on logout Server-side session destruction
JWT Best Practices
Do Do Not
Use asymmetric signing (RS256, ES256) for distributed systems Use none algorithm
Set short expiration (exp ) — 15 minutes for access tokens Store sensitive data in JWT payload
Validate iss , aud , exp , and nbf claims Use JWT as a session replacement for long-lived sessions
Store refresh tokens securely (HttpOnly cookie or server-side) Store JWTs in localStorage (XSS risk)
Rotate signing keys periodically Accept tokens with missing or invalid signatures
Multi-Factor Authentication
-
Support TOTP (Time-based One-Time Password) as baseline MFA.
-
Offer WebAuthn / passkeys as a phishing-resistant option.
-
Provide recovery codes as a backup (store hashed, single-use).
-
Require MFA re-verification for sensitive operations (password change, payment).
Authorization
RBAC (Role-Based Access Control)
// Middleware pattern for RBAC function requireRole(...roles) { return (req, res, next) => { if (!req.user) { return res.status(401).json({ error: 'Authentication required' }); } if (!roles.includes(req.user.role)) { return res.status(403).json({ error: 'Insufficient permissions' }); } next(); }; }
// Usage app.delete('/api/users/:id', authenticate, requireRole('admin'), deleteUser); app.get('/api/reports', authenticate, requireRole('admin', 'manager'), getReports);
ABAC (Attribute-Based Access Control)
// Policy-based authorization function authorize(policy) { return (req, res, next) => { const ctx = { user: req.user, resource: req.resource, action: req.method }; if (!policy.evaluate(ctx)) return res.status(403).json({ error: 'Access denied' }); next(); }; }
// Policy: Users can edit their own posts; admins can edit any post const editPostPolicy = { evaluate: (ctx) => ctx.user.role === 'admin' || ctx.resource.author_id === ctx.user.id, };
Input Validation
SQL Injection
// VULNERABLE
const query = SELECT * FROM products WHERE name = '${userInput}';
// FIXED: Parameterized queries const result = await db.query('SELECT * FROM products WHERE name = $1', [userInput]);
Cross-Site Scripting (XSS)
// VULNERABLE: Inserting user input directly into HTML element.innerHTML = userInput;
// FIXED: Use textContent for plain text element.textContent = userInput;
// FIXED: Use a sanitization library for rich content import DOMPurify from 'dompurify'; element.innerHTML = DOMPurify.sanitize(userInput);
CSRF (Cross-Site Request Forgery)
// Server-side: Generate and validate CSRF tokens const csrf = require('csurf'); const csrfProtection = csrf({ cookie: { httpOnly: true, sameSite: 'strict' } });
app.get('/form', csrfProtection, (req, res) => { res.render('form', { csrfToken: req.csrfToken() }); }); app.post('/submit', csrfProtection, (req, res) => { handleSubmission(req.body); // Token validated automatically by middleware });
// Client-side: <input type="hidden" name="_csrf" value="{{csrfToken}}">
Command Injection
// VULNERABLE: User input passed to shell command
const { exec } = require('child_process');
exec(convert ${userFilename} output.png);
// FIXED: Use execFile with arguments array (no shell interpolation) const { execFile } = require('child_process'); execFile('convert', [userFilename, 'output.png']);
Path Traversal
// VULNERABLE: User controls file path
app.get('/files/:name', (req, res) => res.sendFile(/uploads/${req.params.name}));
// FIXED: Resolve and validate path stays within allowed directory const path = require('path'); app.get('/files/:name', (req, res) => { const safePath = path.resolve('/uploads', req.params.name); if (!safePath.startsWith('/uploads/')) return res.status(400).json({ error: 'Invalid path' }); res.sendFile(safePath); });
Secrets Management
Rules
Rule Details
Never commit secrets to version control Use .gitignore for .env files; use git-secrets or trufflehog to scan
Use environment variables for runtime config Load via .env in development, platform config in production
Rotate secrets regularly Automate rotation where possible (AWS Secrets Manager, HashiCorp Vault)
Use least-privilege credentials Database users should only have necessary grants
Audit secret access Log when secrets are read and by whom
.gitignore Entries for Secrets
.env .env.local .env.production .env.*.local *.pem *.key *.p12 credentials.json service-account.json
HTTPS and CORS
// HTTPS enforcement middleware
app.use((req, res, next) => {
if (req.headers['x-forwarded-proto'] !== 'https' && process.env.NODE_ENV === 'production') {
return res.redirect(301, https://${req.hostname}${req.url});
}
next();
});
// Restrictive CORS — specify exact origins const cors = require('cors'); app.use(cors({ origin: ['https://app.example.com', 'https://admin.example.com'], methods: ['GET', 'POST', 'PUT', 'DELETE'], allowedHeaders: ['Content-Type', 'Authorization'], credentials: true, maxAge: 86400, }));
Security Headers
Implement these headers on all responses:
Header Value Purpose
Strict-Transport-Security
max-age=63072000; includeSubDomains; preload
Force HTTPS
X-Content-Type-Options
nosniff
Prevent MIME-type sniffing
X-Frame-Options
DENY or SAMEORIGIN
Prevent clickjacking
Content-Security-Policy
See below Prevent XSS and data injection
Referrer-Policy
strict-origin-when-cross-origin
Control referrer leakage
Permissions-Policy
camera=(), microphone=(), geolocation=()
Disable unused browser features
X-XSS-Protection
0
Disable legacy XSS filter (CSP supersedes it)
Content Security Policy Example
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{random}'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://cdn.example.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self';
Dependency Vulnerability Scanning
Integrate into CI — fail on high or critical vulnerabilities
npm audit --audit-level=high # Node.js pip-audit # Python bundle audit # Ruby cargo audit # Rust govulncheck ./... # Go snyk test # Multi-language (commercial) trivy fs --security-checks vuln . # Container and filesystem
Security Logging
Log these: authentication attempts (success and failure), authorization failures, input validation failures, rate limit triggers, administrative actions, security setting changes.
Never log these: passwords, full credit card numbers, SSNs, session tokens, API keys, personal health information.
Security Review Checklist
Before merging any code, verify:
-
No secrets or credentials in the codebase.
-
All user inputs validated and sanitized.
-
SQL queries use parameterized statements.
-
Authentication enforced on all protected endpoints.
-
Authorization checked for resource-level access.
-
CSRF protection on state-changing requests.
-
Security headers present on all responses.
-
Dependencies scanned for known vulnerabilities.
-
Error messages do not leak internal details.
-
Logging captures security events without sensitive data.
-
File uploads validated (type, size, name sanitized).
-
Rate limiting on authentication and public endpoints.
-
CORS restricted to known origins.
-
HTTPS enforced with HSTS header.