OWASP Top 10 Security
Prevent common security vulnerabilities in web applications.
OWASP Top 10 (2021)
Vulnerability Prevention
A01 Broken Access Control Proper authorization checks A02 Cryptographic Failures Strong encryption, secure storage A03 Injection Input validation, parameterized queries A04 Insecure Design Threat modeling, secure patterns A05 Security Misconfiguration Hardened configs, no defaults A06 Vulnerable Components Dependency scanning, updates A07 Auth Failures MFA, secure session management A08 Data Integrity Failures Input validation, signed updates A09 Logging Failures Comprehensive audit logs A10 SSRF URL validation, allowlists A01: Broken Access Control Prevention Patterns // ❌ BAD: No authorization check app.get('/api/users/:id', async (req, res) => { const user = await db.users.findById(req.params.id); res.json(user); });
// ✅ GOOD: Verify ownership app.get('/api/users/:id', authenticate, async (req, res) => { const userId = req.params.id;
// Users can only access their own data if (req.user.id !== userId && req.user.role !== 'admin') { return res.status(403).json({ error: 'Forbidden' }); }
const user = await db.users.findById(userId); res.json(user); });
// ✅ GOOD: Role-based access control (RBAC) const requireRole = (...roles: string[]) => { return (req: Request, res: Response, next: NextFunction) => { if (!roles.includes(req.user?.role)) { return res.status(403).json({ error: 'Insufficient permissions' }); } next(); }; };
app.delete('/api/posts/:id', authenticate, requireRole('admin', 'moderator'), deletePost);
Insecure Direct Object Reference (IDOR) // ❌ BAD: Predictable IDs exposed GET /api/invoices/1001 GET /api/invoices/1002 // Can enumerate others' invoices
// ✅ GOOD: Use UUIDs + ownership check app.get('/api/invoices/:id', authenticate, async (req, res) => { const invoice = await db.invoices.findOne({ id: req.params.id, userId: req.user.id, // Enforce ownership });
if (!invoice) { return res.status(404).json({ error: 'Not found' }); }
res.json(invoice); });
A02: Cryptographic Failures Password Hashing import bcrypt from 'bcrypt'; import crypto from 'crypto';
// ✅ Hash passwords with bcrypt const SALT_ROUNDS = 12;
async function hashPassword(password: string): Promise
async function verifyPassword(password: string, hash: string): Promise
// ✅ Secure token generation function generateSecureToken(length = 32): string { return crypto.randomBytes(length).toString('hex'); }
// ✅ Encrypt sensitive data const ALGORITHM = 'aes-256-gcm'; const KEY = crypto.scryptSync(process.env.ENCRYPTION_KEY!, 'salt', 32);
function encrypt(text: string): { encrypted: string; iv: string; tag: string } { const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv);
let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex');
return { encrypted, iv: iv.toString('hex'), tag: cipher.getAuthTag().toString('hex'), }; }
function decrypt(encrypted: string, iv: string, tag: string): string { const decipher = crypto.createDecipheriv(ALGORITHM, KEY, Buffer.from(iv, 'hex')); decipher.setAuthTag(Buffer.from(tag, 'hex'));
let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8');
return decrypted; }
Secure Headers import helmet from 'helmet';
app.use(helmet()); app.use(helmet.hsts({ maxAge: 31536000, includeSubDomains: true })); app.use(helmet.contentSecurityPolicy({ directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'strict-dynamic'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", 'data:', 'https:'], connectSrc: ["'self'"], fontSrc: ["'self'"], objectSrc: ["'none'"], frameAncestors: ["'none'"], }, }));
A03: Injection
SQL Injection Prevention
// ❌ BAD: String concatenation
const query = SELECT * FROM users WHERE email = '${email}';
// ✅ GOOD: Parameterized queries // With Prisma const user = await prisma.user.findUnique({ where: { email } });
// With raw SQL (parameterized) const user = await db.query('SELECT * FROM users WHERE email = $1', [email]);
// With Knex const user = await knex('users').where({ email }).first();
NoSQL Injection Prevention // ❌ BAD: Direct user input in query const user = await User.findOne({ username: req.body.username }); // Attack: { "username": { "$gt": "" } } returns first user
// ✅ GOOD: Validate input type import { z } from 'zod';
const loginSchema = z.object({ username: z.string().min(3).max(50), password: z.string().min(8), });
app.post('/login', async (req, res) => { const { username, password } = loginSchema.parse(req.body); const user = await User.findOne({ username: String(username) }); // ... });
Command Injection Prevention import { execFile } from 'child_process';
// ❌ BAD: Shell injection
exec(convert ${userInput} output.png); // userInput: "; rm -rf /"
// ✅ GOOD: Use execFile with array args execFile('convert', [userInput, 'output.png'], (error, stdout) => { // Safe - arguments are not shell-interpreted });
// ✅ GOOD: Validate and sanitize const allowedFormats = ['png', 'jpg', 'gif']; if (!allowedFormats.includes(format)) { throw new Error('Invalid format'); }
A04: Insecure Design Rate Limiting import rateLimit from 'express-rate-limit';
// General rate limit const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // 100 requests per window standardHeaders: true, legacyHeaders: false, });
// Strict limit for auth endpoints const authLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour max: 5, // 5 failed attempts skipSuccessfulRequests: true, });
app.use('/api/', limiter); app.use('/api/auth/', authLimiter);
Input Validation import { z } from 'zod';
const userSchema = z.object({ email: z.string().email(), password: z.string() .min(8) .regex(/[A-Z]/, 'Must contain uppercase') .regex(/[a-z]/, 'Must contain lowercase') .regex(/[0-9]/, 'Must contain number') .regex(/[^A-Za-z0-9]/, 'Must contain special character'), age: z.number().int().min(13).max(120), role: z.enum(['user', 'admin']).default('user'), });
app.post('/api/users', async (req, res) => { try { const data = userSchema.parse(req.body); // Validated data is safe to use } catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ errors: error.errors }); } throw error; } });
A05: Security Misconfiguration Environment Configuration // ✅ Never expose stack traces in production app.use((err: Error, req: Request, res: Response, next: NextFunction) => { console.error(err.stack); // Log for debugging
res.status(500).json({ error: process.env.NODE_ENV === 'production' ? 'Internal server error' : err.message, }); });
// ✅ Disable sensitive headers app.disable('x-powered-by');
// ✅ Secure cookie configuration app.use(session({ secret: process.env.SESSION_SECRET!, cookie: { secure: process.env.NODE_ENV === 'production', httpOnly: true, sameSite: 'strict', maxAge: 24 * 60 * 60 * 1000, // 24 hours }, resave: false, saveUninitialized: false, }));
A06: Vulnerable Components Dependency Scanning
Check for vulnerabilities
npm audit npm audit fix
Use Snyk for deeper scanning
npx snyk test npx snyk monitor
Keep dependencies updated
npx npm-check-updates -u
// package.json - Use exact versions or ranges { "dependencies": { "express": "^4.18.0", // Minor updates OK "lodash": "4.17.21" // Exact version }, "overrides": { "vulnerable-package": "^2.0.0" // Force safe version } }
A07: Authentication Failures Secure Session Management import jwt from 'jsonwebtoken';
// ✅ JWT with short expiry + refresh tokens function generateTokens(userId: string) { const accessToken = jwt.sign( { userId }, process.env.JWT_SECRET!, { expiresIn: '15m' } // Short-lived );
const refreshToken = jwt.sign( { userId, type: 'refresh' }, process.env.JWT_REFRESH_SECRET!, { expiresIn: '7d' } );
return { accessToken, refreshToken }; }
// ✅ Secure password reset async function initiatePasswordReset(email: string) { const user = await db.users.findByEmail(email); if (!user) return; // Don't reveal if email exists
const token = crypto.randomBytes(32).toString('hex'); const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
await db.passwordResets.create({ userId: user.id, token: hashedToken, expiresAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour });
await sendEmail(email, Reset link: /reset?token=${token});
}
Multi-Factor Authentication import { authenticator } from 'otplib'; import QRCode from 'qrcode';
// Setup TOTP async function setupMFA(userId: string) { const secret = authenticator.generateSecret(); const otpauth = authenticator.keyuri(userId, 'MyApp', secret); const qrCode = await QRCode.toDataURL(otpauth);
await db.users.update(userId, { mfaSecret: encrypt(secret) });
return { qrCode, secret }; }
// Verify TOTP function verifyMFA(token: string, secret: string): boolean { return authenticator.verify({ token, secret }); }
A08: XSS Prevention
// ✅ React auto-escapes by default
const UserProfile = ({ user }) => (
// Safe - auto-escaped );
// ⚠️ Dangerous - avoid if possible
// ✅ Sanitize HTML if needed import DOMPurify from 'dompurify';
const sanitizedHtml = DOMPurify.sanitize(userHtml, { ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'], ALLOWED_ATTR: ['href'], });
// ✅ Content Security Policy app.use(helmet.contentSecurityPolicy({ directives: { scriptSrc: ["'self'"], // No inline scripts styleSrc: ["'self'", "'unsafe-inline'"], }, }));
A09: Logging & Monitoring import winston from 'winston';
const logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports: [ new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.File({ filename: 'combined.log' }), ], });
// ✅ Log security events function logSecurityEvent(event: string, details: object) { logger.warn({ type: 'security', event, ...details, timestamp: new Date().toISOString(), }); }
// Usage logSecurityEvent('failed_login', { email, ip: req.ip, userAgent: req.headers['user-agent'] }); logSecurityEvent('access_denied', { userId, resource, action }); logSecurityEvent('suspicious_activity', { userId, pattern: 'rapid_requests' });
A10: SSRF Prevention import { URL } from 'url';
// ✅ Validate URLs against allowlist const ALLOWED_HOSTS = ['api.example.com', 'cdn.example.com'];
function isAllowedUrl(urlString: string): boolean { try { const url = new URL(urlString);
// Block private IPs
const privatePatterns = [
/^localhost$/i,
/^127\./,
/^10\./,
/^172\.(1[6-9]|2[0-9]|3[01])\./,
/^192\.168\./,
/^0\./,
/^169\.254\./, // Link-local
];
if (privatePatterns.some(p => p.test(url.hostname))) {
return false;
}
// Check allowlist
return ALLOWED_HOSTS.includes(url.hostname);
} catch { return false; } }
app.post('/api/fetch-url', async (req, res) => { const { url } = req.body;
if (!isAllowedUrl(url)) { return res.status(400).json({ error: 'URL not allowed' }); }
const response = await fetch(url); // ... });
Security Checklist
Pre-Deployment Checklist
Authentication
- [ ] Passwords hashed with bcrypt (cost ≥ 12)
- [ ] JWT tokens have short expiry
- [ ] Session cookies are httpOnly, secure, sameSite
- [ ] Rate limiting on auth endpoints
Authorization
- [ ] All endpoints have auth checks
- [ ] RBAC implemented correctly
- [ ] No IDOR vulnerabilities
Input/Output
- [ ] All input validated with Zod/Joi
- [ ] SQL queries parameterized
- [ ] XSS prevented (CSP, escaping)
- [ ] File uploads validated and sandboxed
Infrastructure
- [ ] HTTPS enforced
- [ ] Security headers configured
- [ ] Dependencies audited
- [ ] Secrets in environment variables
Monitoring
- [ ] Security events logged
- [ ] Error monitoring enabled
- [ ] Alerts configured
Resources OWASP Top 10: https://owasp.org/Top10/ OWASP Cheat Sheets: https://cheatsheetseries.owasp.org/ Node.js Security: https://nodejs.org/en/docs/guides/security/ Snyk: https://snyk.io/