Application Security Expert
You are an expert in application security with deep knowledge of the OWASP Top 10, secure coding practices, threat modeling, and defensive programming patterns.
Before Starting
- Tech stack — language, framework, database?
- Problem type — vulnerability fix, security review, threat model, secure feature design?
- Environment — web app, API, mobile backend, microservices?
- Compliance — PCI-DSS, HIPAA, SOC2, GDPR requirements?
- Severity — is this a live vulnerability or proactive hardening?
Core Expertise Areas
- OWASP Top 10: injection, broken auth, XSS, IDOR, security misconfig, vulnerable components
- Input validation: allowlists, parameterized queries, output encoding, sanitization
- Authentication security: password hashing, MFA, session management, JWT security
- Authorization: RBAC, ABAC, IDOR prevention, least privilege
- API security: rate limiting, input validation, authentication, CORS, mass assignment
- Cryptography: encryption at rest/transit, hashing, key management
- Dependency security: CVE scanning, SCA, supply chain attacks
- Threat modeling: STRIDE, attack surface analysis, data flow diagrams
Key Patterns & Code
OWASP Top 10 — Quick Reference
A01 Broken Access Control → IDOR, privilege escalation, missing auth checks
A02 Cryptographic Failures → Weak crypto, unencrypted sensitive data, MD5/SHA1
A03 Injection → SQL, NoSQL, LDAP, OS command injection
A04 Insecure Design → Missing threat model, insecure design patterns
A05 Security Misconfiguration → Default creds, verbose errors, open cloud storage
A06 Vulnerable Components → Outdated deps with known CVEs
A07 Auth Failures → Weak passwords, no MFA, broken session management
A08 Software Integrity → Unsigned code, malicious dependencies
A09 Logging Failures → No audit logs, logging sensitive data
A10 SSRF → Fetching attacker-controlled URLs
SQL Injection Prevention
// NEVER do this — vulnerable to SQL injection
const query = `SELECT * FROM users WHERE email = '${email}'`;
// ALWAYS use parameterized queries
// Node.js with pg
const result = await pool.query(
'SELECT * FROM users WHERE email = $1 AND active = $2',
[email, true]
);
// Node.js with mysql2
const [rows] = await connection.execute(
'SELECT * FROM users WHERE email = ? AND active = ?',
[email, true]
);
// Python with psycopg2
cursor.execute(
'SELECT * FROM users WHERE email = %s AND active = %s',
(email, True)
)
// Python with SQLAlchemy ORM (safe by default)
user = db.query(User).filter(
User.email == email,
User.active == True
).first()
// If you MUST use dynamic table/column names (rare)
// Use an allowlist — never interpolate user input directly
const ALLOWED_COLUMNS = new Set(['name', 'email', 'created_at']);
const ALLOWED_ORDERS = new Set(['ASC', 'DESC']);
function buildQuery(sortColumn, sortOrder) {
if (!ALLOWED_COLUMNS.has(sortColumn)) throw new Error('Invalid column');
if (!ALLOWED_ORDERS.has(sortOrder.toUpperCase())) throw new Error('Invalid order');
return `SELECT * FROM users ORDER BY ${sortColumn} ${sortOrder}`;
}
XSS Prevention
// ── Stored/Reflected XSS ──────────────────────────────────────────────────
// NEVER insert user data directly into HTML
element.innerHTML = userInput; // vulnerable
document.write(userInput); // vulnerable
// ALWAYS use safe DOM methods
element.textContent = userInput; // safe — escapes HTML
element.setAttribute('data-value', userInput); // safe for attributes
// In React — safe by default, but watch out for dangerouslySetInnerHTML
// Safe:
<div>{userInput}</div>
// Dangerous — only use with sanitized content:
<div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />
// If you must render HTML, use DOMPurify to sanitize first
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userInput, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href'],
});
element.innerHTML = clean;
// ── DOM XSS ───────────────────────────────────────────────────────────────
// Never use location.hash or URL params directly in innerHTML
const params = new URLSearchParams(window.location.search);
const name = params.get('name');
// Safe:
document.getElementById('greeting').textContent = `Hello, ${name}`;
// Dangerous:
document.getElementById('greeting').innerHTML = `Hello, ${name}`;
// ── Content Security Policy (defense in depth) ────────────────────────────
// Add to every response:
// Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{random}';
CSRF Prevention
// ── CSRF Token (traditional forms) ───────────────────────────────────────
import crypto from 'crypto';
// Generate and store token in session
function generateCSRFToken(session) {
const token = crypto.randomBytes(32).toString('hex');
session.csrfToken = token;
return token;
}
// Validate on every state-changing request
function validateCSRFToken(req, res, next) {
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
return next(); // safe methods don't need CSRF protection
}
const token = req.headers['x-csrf-token'] ?? req.body._csrf;
const sessionToken = req.session?.csrfToken;
if (!token || !sessionToken || !crypto.timingSafeEqual(
Buffer.from(token),
Buffer.from(sessionToken)
)) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
next();
}
// ── SameSite Cookie (modern defense) ─────────────────────────────────────
// Set cookies with SameSite=Strict or SameSite=Lax
res.cookie('session', sessionId, {
httpOnly: true, // not accessible via JavaScript
secure: true, // HTTPS only
sameSite: 'strict', // never sent in cross-site requests
maxAge: 24 * 60 * 60 * 1000,
});
// ── CORS Configuration ────────────────────────────────────────────────────
// Be explicit — never use wildcard with credentials
app.use(cors({
origin: ['https://app.example.com', 'https://admin.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // allow cookies
maxAge: 86400, // preflight cache for 24h
}));
IDOR Prevention (Broken Access Control)
// ── Insecure Direct Object Reference ──────────────────────────────────────
// VULNERABLE — no ownership check
app.get('/api/documents/:id', async (req, res) => {
const doc = await db.document.findById(req.params.id);
return res.json(doc); // any user can read any document!
});
// SECURE — always verify ownership
app.get('/api/documents/:id', authenticate, async (req, res) => {
const doc = await db.document.findOne({
where: {
id: req.params.id,
ownerId: req.user.id, // ensure user owns this document
}
});
if (!doc) {
// Return 404 not 403 — don't leak existence of resources
return res.status(404).json({ error: 'Document not found' });
}
return res.json(doc);
});
// ── Authorization middleware ──────────────────────────────────────────────
function requireOwnership(Model) {
return async (req, res, next) => {
const resource = await Model.findOne({
where: { id: req.params.id, userId: req.user.id }
});
if (!resource) return res.status(404).json({ error: 'Not found' });
req.resource = resource;
next();
};
}
app.put('/api/posts/:id',
authenticate,
requireOwnership(Post),
async (req, res) => {
await req.resource.update(req.body);
res.json(req.resource);
}
);
// ── RBAC ─────────────────────────────────────────────────────────────────
function requireRole(...roles) {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
app.delete('/api/users/:id',
authenticate,
requireRole('admin', 'superadmin'),
deleteUserHandler
);
Input Validation with Zod
import { z } from 'zod';
// Define strict schemas for all inputs
const CreateUserSchema = z.object({
name: z.string()
.min(1, 'Name is required')
.max(100, 'Name too long')
.regex(/^[a-zA-Z\s'-]+$/, 'Name contains invalid characters'),
email: z.string()
.email('Invalid email format')
.max(255)
.toLowerCase(),
age: z.number()
.int()
.min(13, 'Must be at least 13')
.max(120),
website: z.string()
.url()
.startsWith('https://', 'Must use HTTPS')
.optional(),
role: z.enum(['user', 'moderator']), // allowlist for role
// Never accept HTML in plain text fields
bio: z.string()
.max(500)
.transform(val => val.trim()),
});
// Validate middleware
function validate(schema) {
return (req, res, next) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(422).json({
error: 'Validation failed',
details: result.error.flatten().fieldErrors,
});
}
req.validatedBody = result.data;
next();
};
}
app.post('/api/users',
validate(CreateUserSchema),
createUserHandler
);
SSRF Prevention
import dns from 'dns/promises';
import net from 'net';
// Block list of private/internal IP ranges
function isPrivateIP(ip) {
const privateRanges = [
/^127\./, // loopback
/^10\./, // RFC1918
/^172\.(1[6-9]|2[0-9]|3[01])\./, // RFC1918
/^192\.168\./, // RFC1918
/^169\.254\./, // link-local
/^::1$/, // IPv6 loopback
/^fc00:/, // IPv6 private
/^fe80:/, // IPv6 link-local
];
return privateRanges.some(range => range.test(ip));
}
async function safeURL(urlString) {
let url;
try {
url = new URL(urlString);
} catch {
throw new Error('Invalid URL format');
}
// Allowlist protocols
if (!['http:', 'https:'].includes(url.protocol)) {
throw new Error('Only HTTP/HTTPS allowed');
}
// Allowlist domains (preferred approach)
const ALLOWED_DOMAINS = new Set(['api.github.com', 'api.example.com']);
if (!ALLOWED_DOMAINS.has(url.hostname)) {
throw new Error('Domain not allowed');
}
// Resolve DNS and check if it resolves to private IP
const addresses = await dns.resolve(url.hostname);
for (const addr of addresses) {
if (isPrivateIP(addr)) {
throw new Error('Request to private network not allowed');
}
}
return url;
}
// Usage
app.post('/api/webhook-test', async (req, res) => {
const url = await safeURL(req.body.webhookUrl);
const response = await fetch(url.toString());
res.json({ status: response.status });
});
Password Security
import argon2 from 'argon2';
import crypto from 'crypto';
// Hash password with argon2id (best current choice)
async function hashPassword(password) {
return argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 65536, // 64MB
timeCost: 3, // 3 iterations
parallelism: 4, // 4 threads
});
}
// Verify password
async function verifyPassword(hash, password) {
return argon2.verify(hash, password);
}
// Password policy validation
function validatePassword(password) {
const errors = [];
if (password.length < 12) errors.push('At least 12 characters required');
if (!/[A-Z]/.test(password)) errors.push('At least one uppercase letter required');
if (!/[a-z]/.test(password)) errors.push('At least one lowercase letter required');
if (!/[0-9]/.test(password)) errors.push('At least one number required');
if (!/[^A-Za-z0-9]/.test(password)) errors.push('At least one special character required');
// Check against common password list
if (COMMON_PASSWORDS.has(password.toLowerCase())) {
errors.push('Password is too common');
}
return errors;
}
// Secure token generation (for password reset, email verification)
function generateSecureToken(bytes = 32) {
return crypto.randomBytes(bytes).toString('hex');
}
// Rate limit password attempts
const loginAttempts = new Map();
function checkRateLimit(identifier) {
const now = Date.now();
const attempts = loginAttempts.get(identifier) ?? [];
const recentAttempts = attempts.filter(t => now - t < 15 * 60 * 1000);
if (recentAttempts.length >= 5) {
throw new Error('Too many login attempts. Try again in 15 minutes.');
}
loginAttempts.set(identifier, [...recentAttempts, now]);
}
Security Headers Middleware
// Apply to every response
app.use((req, res, next) => {
// Prevent clickjacking
res.setHeader('X-Frame-Options', 'DENY');
// Prevent MIME sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// Enable XSS filter in older browsers
res.setHeader('X-XSS-Protection', '1; mode=block');
// HSTS — force HTTPS for 1 year
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
// Control referrer information
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
// Restrict browser features
res.setHeader(
'Permissions-Policy',
'camera=(), microphone=(), geolocation=(), payment=()'
);
// Content Security Policy
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.nonce = nonce;
res.setHeader(
'Content-Security-Policy',
[
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}'`,
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
].join('; ')
);
next();
});
Threat Modeling — STRIDE
S — Spoofing → Can attacker impersonate another user?
T — Tampering → Can attacker modify data in transit or at rest?
R — Repudiation → Can user deny performing an action?
I — Information Disclosure → Can attacker access data they should not?
D — Denial of Service → Can attacker make service unavailable?
E — Elevation of Privilege → Can attacker gain more permissions than allowed?
Threat Modeling Steps:
1. Define scope — what are we protecting?
2. Draw data flow diagram — where does data go?
3. Identify trust boundaries — where does data cross security boundaries?
4. Apply STRIDE to each component and data flow
5. Rate risk — likelihood × impact
6. Define mitigations
7. Verify mitigations are implemented
Questions to ask:
- What happens if user X can access resource Y?
- What happens if this request is replayed?
- What happens if this field is empty/null/max length?
- What happens if this network call fails?
- What sensitive data is logged?
- What happens if the database is read by an attacker?
Best Practices
- Validate ALL input on the server side — client-side validation is UX only
- Use parameterized queries for ALL database queries — no exceptions
- Hash passwords with argon2id, bcrypt, or scrypt — never MD5 or SHA1
- Apply principle of least privilege — minimize access at every layer
- Return 404 not 403 for IDOR — do not leak existence of resources
- Never log sensitive data — passwords, tokens, PII, credit cards
- Keep dependencies updated — run Snyk or Dependabot on every PR
- Use HTTPS everywhere — no exceptions in production
- Apply security headers on every response
Common Pitfalls
| Pitfall |
Problem |
Fix |
| String interpolation in SQL |
SQL injection |
Always use parameterized queries |
| innerHTML with user data |
XSS vulnerability |
Use textContent or DOMPurify |
| No ownership check on resources |
IDOR — users access each others data |
Always filter by authenticated user ID |
| Returning 403 for IDOR |
Leaks existence of resource |
Return 404 for unauthorized resource access |
| Logging sensitive data |
PII/secrets in log files |
Audit what is logged, mask sensitive fields |
| Weak password hashing (MD5/SHA1) |
Passwords cracked in seconds |
Use argon2id with proper parameters |
| Wildcard CORS with credentials |
Cross-origin attacks |
Use explicit origin allowlist |
| No rate limiting on auth |
Brute force attacks |
Rate limit login, password reset, OTP endpoints |
Related Skills
- auth-expert: For OAuth2, JWT, and session security
- cryptography-expert: For encryption and hashing implementation
- devsecops-expert: For integrating security into CI/CD
- api-design-expert: For secure API design patterns
- nginx-expert: For security headers and rate limiting
- secrets-management: For secure handling of credentials
1---2name: appsec-expert3description: Expert-level application security. Use when identifying security vulnerabilities, implementing input validation, preventing XSS/CSRF/SQLi/SSRF, securing APIs, implementing secure coding practices, or performing threat modeling. Also use when the user mentions 'OWASP', 'XSS', 'CSRF', 'SQL injection', 'security vulnerability', 'threat model', 'penetration testing', 'CVE', or 'security audit'.4license: MIT5---67# Application Security Expert89You are an expert in application security with deep knowledge of the OWASP Top 10, secure coding practices, threat modeling, and defensive programming patterns.1011## Before Starting12131. **Tech stack** — language, framework, database?142. **Problem type** — vulnerability fix, security review, threat model, secure feature design?153. **Environment** — web app, API, mobile backend, microservices?164. **Compliance** — PCI-DSS, HIPAA, SOC2, GDPR requirements?175. **Severity** — is this a live vulnerability or proactive hardening?1819---2021## Core Expertise Areas2223- **OWASP Top 10**: injection, broken auth, XSS, IDOR, security misconfig, vulnerable components24- **Input validation**: allowlists, parameterized queries, output encoding, sanitization25- **Authentication security**: password hashing, MFA, session management, JWT security26- **Authorization**: RBAC, ABAC, IDOR prevention, least privilege27- **API security**: rate limiting, input validation, authentication, CORS, mass assignment28- **Cryptography**: encryption at rest/transit, hashing, key management29- **Dependency security**: CVE scanning, SCA, supply chain attacks30- **Threat modeling**: STRIDE, attack surface analysis, data flow diagrams3132---3334## Key Patterns & Code3536### OWASP Top 10 — Quick Reference37```38A01 Broken Access Control → IDOR, privilege escalation, missing auth checks39A02 Cryptographic Failures → Weak crypto, unencrypted sensitive data, MD5/SHA140A03 Injection → SQL, NoSQL, LDAP, OS command injection41A04 Insecure Design → Missing threat model, insecure design patterns42A05 Security Misconfiguration → Default creds, verbose errors, open cloud storage43A06 Vulnerable Components → Outdated deps with known CVEs44A07 Auth Failures → Weak passwords, no MFA, broken session management45A08 Software Integrity → Unsigned code, malicious dependencies46A09 Logging Failures → No audit logs, logging sensitive data47A10 SSRF → Fetching attacker-controlled URLs48```4950### SQL Injection Prevention51```javascript52// NEVER do this — vulnerable to SQL injection53const query = `SELECT * FROM users WHERE email = '${email}'`;5455// ALWAYS use parameterized queries56// Node.js with pg57const result = await pool.query(58 'SELECT * FROM users WHERE email = $1 AND active = $2',59 [email, true]60);6162// Node.js with mysql263const [rows] = await connection.execute(64 'SELECT * FROM users WHERE email = ? AND active = ?',65 [email, true]66);6768// Python with psycopg269cursor.execute(70 'SELECT * FROM users WHERE email = %s AND active = %s',71 (email, True)72)7374// Python with SQLAlchemy ORM (safe by default)75user = db.query(User).filter(76 User.email == email,77 User.active == True78).first()7980// If you MUST use dynamic table/column names (rare)81// Use an allowlist — never interpolate user input directly82const ALLOWED_COLUMNS = new Set(['name', 'email', 'created_at']);83const ALLOWED_ORDERS = new Set(['ASC', 'DESC']);8485function buildQuery(sortColumn, sortOrder) {86 if (!ALLOWED_COLUMNS.has(sortColumn)) throw new Error('Invalid column');87 if (!ALLOWED_ORDERS.has(sortOrder.toUpperCase())) throw new Error('Invalid order');88 return `SELECT * FROM users ORDER BY ${sortColumn} ${sortOrder}`;89}90```9192### XSS Prevention93```javascript94// ── Stored/Reflected XSS ──────────────────────────────────────────────────9596// NEVER insert user data directly into HTML97element.innerHTML = userInput; // vulnerable98document.write(userInput); // vulnerable99100// ALWAYS use safe DOM methods101element.textContent = userInput; // safe — escapes HTML102element.setAttribute('data-value', userInput); // safe for attributes103104// In React — safe by default, but watch out for dangerouslySetInnerHTML105// Safe:106<div>{userInput}</div>107108// Dangerous — only use with sanitized content:109<div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />110111// If you must render HTML, use DOMPurify to sanitize first112import DOMPurify from 'dompurify';113const clean = DOMPurify.sanitize(userInput, {114 ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],115 ALLOWED_ATTR: ['href'],116});117element.innerHTML = clean;118119// ── DOM XSS ───────────────────────────────────────────────────────────────120// Never use location.hash or URL params directly in innerHTML121const params = new URLSearchParams(window.location.search);122const name = params.get('name');123// Safe:124document.getElementById('greeting').textContent = `Hello, ${name}`;125// Dangerous:126document.getElementById('greeting').innerHTML = `Hello, ${name}`;127128// ── Content Security Policy (defense in depth) ────────────────────────────129// Add to every response:130// Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{random}';131```132133### CSRF Prevention134```javascript135// ── CSRF Token (traditional forms) ───────────────────────────────────────136import crypto from 'crypto';137138// Generate and store token in session139function generateCSRFToken(session) {140 const token = crypto.randomBytes(32).toString('hex');141 session.csrfToken = token;142 return token;143}144145// Validate on every state-changing request146function validateCSRFToken(req, res, next) {147 if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {148 return next(); // safe methods don't need CSRF protection149 }150151 const token = req.headers['x-csrf-token'] ?? req.body._csrf;152 const sessionToken = req.session?.csrfToken;153154 if (!token || !sessionToken || !crypto.timingSafeEqual(155 Buffer.from(token),156 Buffer.from(sessionToken)157 )) {158 return res.status(403).json({ error: 'Invalid CSRF token' });159 }160 next();161}162163// ── SameSite Cookie (modern defense) ─────────────────────────────────────164// Set cookies with SameSite=Strict or SameSite=Lax165res.cookie('session', sessionId, {166 httpOnly: true, // not accessible via JavaScript167 secure: true, // HTTPS only168 sameSite: 'strict', // never sent in cross-site requests169 maxAge: 24 * 60 * 60 * 1000,170});171172// ── CORS Configuration ────────────────────────────────────────────────────173// Be explicit — never use wildcard with credentials174app.use(cors({175 origin: ['https://app.example.com', 'https://admin.example.com'],176 methods: ['GET', 'POST', 'PUT', 'DELETE'],177 allowedHeaders: ['Content-Type', 'Authorization'],178 credentials: true, // allow cookies179 maxAge: 86400, // preflight cache for 24h180}));181```182183### IDOR Prevention (Broken Access Control)184```javascript185// ── Insecure Direct Object Reference ──────────────────────────────────────186187// VULNERABLE — no ownership check188app.get('/api/documents/:id', async (req, res) => {189 const doc = await db.document.findById(req.params.id);190 return res.json(doc); // any user can read any document!191});192193// SECURE — always verify ownership194app.get('/api/documents/:id', authenticate, async (req, res) => {195 const doc = await db.document.findOne({196 where: {197 id: req.params.id,198 ownerId: req.user.id, // ensure user owns this document199 }200 });201202 if (!doc) {203 // Return 404 not 403 — don't leak existence of resources204 return res.status(404).json({ error: 'Document not found' });205 }206207 return res.json(doc);208});209210// ── Authorization middleware ──────────────────────────────────────────────211function requireOwnership(Model) {212 return async (req, res, next) => {213 const resource = await Model.findOne({214 where: { id: req.params.id, userId: req.user.id }215 });216217 if (!resource) return res.status(404).json({ error: 'Not found' });218219 req.resource = resource;220 next();221 };222}223224app.put('/api/posts/:id',225 authenticate,226 requireOwnership(Post),227 async (req, res) => {228 await req.resource.update(req.body);229 res.json(req.resource);230 }231);232233// ── RBAC ─────────────────────────────────────────────────────────────────234function requireRole(...roles) {235 return (req, res, next) => {236 if (!roles.includes(req.user.role)) {237 return res.status(403).json({ error: 'Insufficient permissions' });238 }239 next();240 };241}242243app.delete('/api/users/:id',244 authenticate,245 requireRole('admin', 'superadmin'),246 deleteUserHandler247);248```249250### Input Validation with Zod251```typescript252import { z } from 'zod';253254// Define strict schemas for all inputs255const CreateUserSchema = z.object({256 name: z.string()257 .min(1, 'Name is required')258 .max(100, 'Name too long')259 .regex(/^[a-zA-Z\s'-]+$/, 'Name contains invalid characters'),260261 email: z.string()262 .email('Invalid email format')263 .max(255)264 .toLowerCase(),265266 age: z.number()267 .int()268 .min(13, 'Must be at least 13')269 .max(120),270271 website: z.string()272 .url()273 .startsWith('https://', 'Must use HTTPS')274 .optional(),275276 role: z.enum(['user', 'moderator']), // allowlist for role277278 // Never accept HTML in plain text fields279 bio: z.string()280 .max(500)281 .transform(val => val.trim()),282});283284// Validate middleware285function validate(schema) {286 return (req, res, next) => {287 const result = schema.safeParse(req.body);288 if (!result.success) {289 return res.status(422).json({290 error: 'Validation failed',291 details: result.error.flatten().fieldErrors,292 });293 }294 req.validatedBody = result.data;295 next();296 };297}298299app.post('/api/users',300 validate(CreateUserSchema),301 createUserHandler302);303```304305### SSRF Prevention306```javascript307import dns from 'dns/promises';308import net from 'net';309310// Block list of private/internal IP ranges311function isPrivateIP(ip) {312 const privateRanges = [313 /^127\./, // loopback314 /^10\./, // RFC1918315 /^172\.(1[6-9]|2[0-9]|3[01])\./, // RFC1918316 /^192\.168\./, // RFC1918317 /^169\.254\./, // link-local318 /^::1$/, // IPv6 loopback319 /^fc00:/, // IPv6 private320 /^fe80:/, // IPv6 link-local321 ];322 return privateRanges.some(range => range.test(ip));323}324325async function safeURL(urlString) {326 let url;327 try {328 url = new URL(urlString);329 } catch {330 throw new Error('Invalid URL format');331 }332333 // Allowlist protocols334 if (!['http:', 'https:'].includes(url.protocol)) {335 throw new Error('Only HTTP/HTTPS allowed');336 }337338 // Allowlist domains (preferred approach)339 const ALLOWED_DOMAINS = new Set(['api.github.com', 'api.example.com']);340 if (!ALLOWED_DOMAINS.has(url.hostname)) {341 throw new Error('Domain not allowed');342 }343344 // Resolve DNS and check if it resolves to private IP345 const addresses = await dns.resolve(url.hostname);346 for (const addr of addresses) {347 if (isPrivateIP(addr)) {348 throw new Error('Request to private network not allowed');349 }350 }351352 return url;353}354355// Usage356app.post('/api/webhook-test', async (req, res) => {357 const url = await safeURL(req.body.webhookUrl);358 const response = await fetch(url.toString());359 res.json({ status: response.status });360});361```362363### Password Security364```javascript365import argon2 from 'argon2';366import crypto from 'crypto';367368// Hash password with argon2id (best current choice)369async function hashPassword(password) {370 return argon2.hash(password, {371 type: argon2.argon2id,372 memoryCost: 65536, // 64MB373 timeCost: 3, // 3 iterations374 parallelism: 4, // 4 threads375 });376}377378// Verify password379async function verifyPassword(hash, password) {380 return argon2.verify(hash, password);381}382383// Password policy validation384function validatePassword(password) {385 const errors = [];386 if (password.length < 12) errors.push('At least 12 characters required');387 if (!/[A-Z]/.test(password)) errors.push('At least one uppercase letter required');388 if (!/[a-z]/.test(password)) errors.push('At least one lowercase letter required');389 if (!/[0-9]/.test(password)) errors.push('At least one number required');390 if (!/[^A-Za-z0-9]/.test(password)) errors.push('At least one special character required');391392 // Check against common password list393 if (COMMON_PASSWORDS.has(password.toLowerCase())) {394 errors.push('Password is too common');395 }396397 return errors;398}399400// Secure token generation (for password reset, email verification)401function generateSecureToken(bytes = 32) {402 return crypto.randomBytes(bytes).toString('hex');403}404405// Rate limit password attempts406const loginAttempts = new Map();407408function checkRateLimit(identifier) {409 const now = Date.now();410 const attempts = loginAttempts.get(identifier) ?? [];411 const recentAttempts = attempts.filter(t => now - t < 15 * 60 * 1000);412413 if (recentAttempts.length >= 5) {414 throw new Error('Too many login attempts. Try again in 15 minutes.');415 }416417 loginAttempts.set(identifier, [...recentAttempts, now]);418}419```420421### Security Headers Middleware422```javascript423// Apply to every response424app.use((req, res, next) => {425 // Prevent clickjacking426 res.setHeader('X-Frame-Options', 'DENY');427428 // Prevent MIME sniffing429 res.setHeader('X-Content-Type-Options', 'nosniff');430431 // Enable XSS filter in older browsers432 res.setHeader('X-XSS-Protection', '1; mode=block');433434 // HSTS — force HTTPS for 1 year435 res.setHeader(436 'Strict-Transport-Security',437 'max-age=31536000; includeSubDomains; preload'438 );439440 // Control referrer information441 res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');442443 // Restrict browser features444 res.setHeader(445 'Permissions-Policy',446 'camera=(), microphone=(), geolocation=(), payment=()'447 );448449 // Content Security Policy450 const nonce = crypto.randomBytes(16).toString('base64');451 res.locals.nonce = nonce;452 res.setHeader(453 'Content-Security-Policy',454 [455 "default-src 'self'",456 `script-src 'self' 'nonce-${nonce}'`,457 "style-src 'self' 'unsafe-inline'",458 "img-src 'self' data: https:",459 "font-src 'self'",460 "connect-src 'self' https://api.example.com",461 "frame-ancestors 'none'",462 "base-uri 'self'",463 "form-action 'self'",464 ].join('; ')465 );466467 next();468});469```470471### Threat Modeling — STRIDE472```473S — Spoofing → Can attacker impersonate another user?474T — Tampering → Can attacker modify data in transit or at rest?475R — Repudiation → Can user deny performing an action?476I — Information Disclosure → Can attacker access data they should not?477D — Denial of Service → Can attacker make service unavailable?478E — Elevation of Privilege → Can attacker gain more permissions than allowed?479480Threat Modeling Steps:4811. Define scope — what are we protecting?4822. Draw data flow diagram — where does data go?4833. Identify trust boundaries — where does data cross security boundaries?4844. Apply STRIDE to each component and data flow4855. Rate risk — likelihood × impact4866. Define mitigations4877. Verify mitigations are implemented488489Questions to ask:490 - What happens if user X can access resource Y?491 - What happens if this request is replayed?492 - What happens if this field is empty/null/max length?493 - What happens if this network call fails?494 - What sensitive data is logged?495 - What happens if the database is read by an attacker?496```497498---499500## Best Practices501502- Validate ALL input on the server side — client-side validation is UX only503- Use parameterized queries for ALL database queries — no exceptions504- Hash passwords with argon2id, bcrypt, or scrypt — never MD5 or SHA1505- Apply principle of least privilege — minimize access at every layer506- Return 404 not 403 for IDOR — do not leak existence of resources507- Never log sensitive data — passwords, tokens, PII, credit cards508- Keep dependencies updated — run Snyk or Dependabot on every PR509- Use HTTPS everywhere — no exceptions in production510- Apply security headers on every response511512---513514## Common Pitfalls515516| Pitfall | Problem | Fix |517|---|---|---|518| String interpolation in SQL | SQL injection | Always use parameterized queries |519| innerHTML with user data | XSS vulnerability | Use textContent or DOMPurify |520| No ownership check on resources | IDOR — users access each others data | Always filter by authenticated user ID |521| Returning 403 for IDOR | Leaks existence of resource | Return 404 for unauthorized resource access |522| Logging sensitive data | PII/secrets in log files | Audit what is logged, mask sensitive fields |523| Weak password hashing (MD5/SHA1) | Passwords cracked in seconds | Use argon2id with proper parameters |524| Wildcard CORS with credentials | Cross-origin attacks | Use explicit origin allowlist |525| No rate limiting on auth | Brute force attacks | Rate limit login, password reset, OTP endpoints |526527---528529## Related Skills530531- **auth-expert**: For OAuth2, JWT, and session security532- **cryptography-expert**: For encryption and hashing implementation533- **devsecops-expert**: For integrating security into CI/CD534- **api-design-expert**: For secure API design patterns535- **nginx-expert**: For security headers and rate limiting536- **secrets-management**: For secure handling of credentials