Web Security: OWASP Top 10 — Practical Defense Guide (2026)
Security vulnerabilities follow patterns. The OWASP Top 10 lists the most critical ones — and each has a clear defense strategy.
#1 Broken Access Control
// ❌ Vulnerable: Anyone can access any user's data
app.get('/api/users/:id', (req, res) => {
const user = await db.users.findById(req.params.id);
res.json(user); // No ownership check!
});
// ✅ Secure: Enforce resource ownership
app.get('/api/users/:id', authRequired, async (req, res) => {
if (req.params.id !== req.user.id && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Forbidden' });
}
const user = await db.users.findById(req.params.id);
if (!user) return res.status(404).json({ error: 'Not found' });
res.json({ data: user });
});
// Reusable middleware:
function requireOwnership(getResourceFn) {
return async (req, res, next) => {
const resource = await getResourceFn(req.params.id);
if (!resource) return res.status(404).json({ error: 'Not found' });
const isOwner = resource.userId?.toString() === req.user?.id;
const isAdmin = req.user?.role === 'admin';
if (!isOwner && !isAdmin) {
return res.status(403).json({ error: 'Access denied' });
}
req.resource = resource;
next();
};
}
// Usage: app.put('/posts/:id', auth, requireOwnership(findPost), updatePost)
#2 Cryptographic Failures
// ❌ Storing passwords in plain text or weak hash
const hashed = md5(password); // NEVER!
const hashed = sha1(password); // NEVER!
// ✅ Use bcrypt/argon2id for passwords
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 12;
async function hashPassword(password) {
return bcrypt.hash(password, SALT_ROUNDS); // Auto-salts + slow by design
}
async function verifyPassword(inputPassword, storedHash) {
return bcrypt.compare(inputPassword, storedHash); // Timing-safe comparison
}
// ✅ Encrypt sensitive data at rest (API keys, tokens):
const crypto = require('crypto');
function encrypt(text, keyHex) {
const iv = crypto.randomBytes(16); // Unique IV per encryption!
const cipher = crypto.createCipheriv('aes-256-gcm', Buffer.from(keyHex, 'hex'), iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag(); // Integrity verification
return { encrypted, iv: iv.toString('hex'), authTag: authTag.toString('hex') };
}
function decrypt(encryptedData, keyHex) {
const decipher = crypto.createDecipheriv(
'aes-256-gcm', Buffer.from(keyHex, 'hex'),
Buffer.from(encryptedData.iv, 'hex')
);
decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'));
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
#3 Injection
// SQL Injection:
// ❌ String concatenation (vulnerable to: ' OR 1=1 --)
const query = `SELECT * FROM users WHERE email = '${email}'`;
// ✅ Parameterized queries (always!)
const result = await db.query(
'SELECT * FROM users WHERE email = $1 AND role = $2',
[email, role]
);
// NoSQL Injection (MongoDB):
// ❌ Direct object from request body
const user = await db.users.findOne({ username: userInput, password: passInput });
// ✅ Schema validation first, then whitelist fields
const schema = joi.object({
username: joi.string().alphanum().min(3).max(30),
password: joi.string().min(12),
});
const { value } = schema.validate({ username: userInput, password: passInput });
const user = await db.users.findOne(value);
// Command Injection:
// ❌ User input in shell command
execSync(`convert ${filename} output.png`);
// ✅ Use library APIs instead of shell commands
const sharp = require('sharp');
await sharp(filename).png().toFile('output.png');
// XSS Prevention:
// In templates: auto-escape by default (EJS <%= %>, React JSX)
// For dynamic HTML:
import DOMPurify from 'dompurify';
const cleanHTML = DOMPurify.sanitize(dirtyHTML, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href'],
});
#4 Insecure Design
// ❌ Password reset token in URL (logged everywhere)
app.post('/api/reset-password', async (req, res) => {
const token = generateToken();
await sendEmail(email, `Click: https://example.com/reset?token=${token}`);
});
// ✅ Token only used via POST form (not logged in URLs/referrers)
app.post('/api/reset-password', async (req, res) => {
const token = crypto.randomBytes(32).toString('hex');
await db.tokens.create({ token, email, expiresAt: Date.now() + 3600000 });
await sendEmail(email, 'Visit: https://example.com/reset-form');
// Token submitted via POST form, not URL parameter
});
// ❌ Sequential IDs reveal business data
GET /api/invoices/1001 → /api/invoices/1002 (competitor knows your volume!)
// ✅ Use UUIDs or non-sequential identifiers
const invoiceId = crypto.randomUUID();
#5 Security Misconfiguration
// ❌ Exposing error details to users
app.use((err, req, res, next) => {
res.status(500).json({ error: err.message, stack: err.stack }); // Leaks internals!
});
// ✅ Generic error messages in production
app.use((err, req, res, next) => {
const incidentId = crypto.randomUUID();
logger.error(`[${incidentId}]`, err); // Log full error server-side
res.status(err.statusCode || 500).json({
error: process.env.NODE_ENV === 'production' ? 'Internal error' : err.message,
...(process.env.NODE_ENV === 'production' && { incidentId }),
});
});
// ✅ Comprehensive security headers via helmet:
const helmet = require('helmet');
app.use(helmet({
contentSecurityPolicy: {
directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'"] },
},
hsts: { maxAge: 31536000, includeSubDomains: true },
noSniff: true,
frameOptions: { action: 'deny' }, // Prevent clickjacking
}));
// ✅ Rate limiting:
const rateLimit = require('express-rate-limit');
app.use('/api/auth/', rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
message: { error: 'Too many attempts. Please try again later.' },
}));
#6-10 Quick Reference
#6 Vulnerable & Outdated Components
→ npm audit && npm audit fix (run regularly!)
→ Dependabot / Renovate bot for automated updates
#7 Identification & Authentication Failures
→ Strong password policy (12+ chars, mixed types)
→ Implement account lockout after failed attempts
→ Regenerate session on login (prevent session fixation)
→ Use secure session cookies (httpOnly, sameSite=strict, secure)
#8 Software & Data Integrity Failures
→ Subresource Integrity (SRI) for CDN resources
→ <script src="cdn.js" integrity="sha256-xxx" crossorigin="anonymous">
→ Verify digital signatures for dependencies
#9 Security Logging & Monitoring Failures
→ Log ALL security events: login success/failure, permission denied, rate limit exceeded
→ Include context: IP, userId, timestamp, request ID
→ Set up alerts for anomaly patterns (100 failures in 1 minute from same IP)
#10 Server-Side Request Forgery (SSRF)
→ Never let user-controlled URLs into backend fetch()
→ Validate and sanitize URLs (block internal IPs: 127.x, 10.x, 172.16-31.x, 192.168.x)
→ Use allowlist of permitted domains when possible
Which OWASP vulnerability have you encountered most? What's your security tip?
Follow @armorbreak for more practical developer guides.
Top comments (0)