Web Security: OWASP Top 10 and How to Fix Them (2026)
Security isn't a feature you add later — it's built into every layer. Here's how the top 10 vulnerabilities work and how to fix them in your code.
1. Broken Access Control
// ❌ Vulnerable: Anyone can access any user's data
app.get('/api/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
res.json(user); // No check if requester owns this data!
});
// ✅ Secure: Enforce 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);
res.json({ data: user });
});
// Middleware pattern for consistent access control:
function requireOwnership(resource) {
return async (req, res, next) => {
const item = await resource.findById(req.params.id);
if (!item) return res.status(404).json({ error: 'Not found' });
const isOwner = item.userId?.toString() === req.user.id;
const isAdmin = req.user.role === 'admin';
if (!isOwner && !isAdmin) {
return res.status(403).json({ error: 'Access denied' });
}
req.resource = item;
next();
};
}
// Usage:
app.put('/api/posts/:id', authRequired, requireOwnership(Post), updatePost);
2. Cryptographic Failures
// ❌ Storing passwords in plain text or weak hashing:
const hashedPassword = md5(password); // NEVER!
const hashedPassword = sha1(password); // NEVER!
// ✅ Use bcrypt/argonid (purpose-built for passwords):
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 12;
async function hashPassword(password) {
return await bcrypt.hash(password, SALT_ROUNDS);
}
async function verifyPassword(inputPassword, hash) {
return await bcrypt.compare(inputPassword, hash); // Handles salt automatically
}
// For other sensitive data (API keys, tokens):
const { createCipheriv, randomBytes, createDecipheriv } = require('crypto');
function encrypt(text, keyHex) {
const iv = randomBytes(16); // Unique IV per encryption!
const cipher = 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 = 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, NoSQL, Command)
// === SQL Injection ===
// ❌ String concatenation (vulnerable):
const query = `SELECT * FROM users WHERE email = '${email}'`;
// Input: "' OR 1=1 --" → bypasses authentication!
// ✅ Parameterized queries (always!):
const user = await db.query(
'SELECT * FROM users WHERE email = $1 AND password_hash = $2',
[email, passwordHash]
);
// === NoSQL Injection ===
// ❌ MongoDB injection via $where/$gt operators:
const user = await db.users.findOne({ username: userInput, password: passInput });
// ✅ Sanitize or use strict schema validation:
const schema = joi.object({
username: joi.string().alphanum().min(3).max(30).required(),
password: joi.string().min(8).required(),
});
const { value } = schema.validate({ username: userInput, password: passInput });
const user = await db.users.findOne(value);
// === Command Injection ===
// ❌ Never pass user input to shell commands:
execSync(`convert ${filename} output.png`);
// ✅ Use library APIs instead:
const sharp = require('sharp');
await sharp(filename).png().toFile('output.png');
4. Insecure Design
// ❌ Password reset token sent in URL (logged everywhere)
app.post('/api/reset-password', async (req, res) => {
const token = generateToken();
await sendEmail(req.email, `Click: https://example.com/reset?token=${token}`);
});
// ✅ Secure design: Token only works via POST form
app.post('/api/reset-password', async (req, res) => {
const token = crypto.randomBytes(32).toString('hex');
await db.tokens.create({ token, email: req.email, expiresAt: Date.now() + 3600000 });
await sendEmail(req.email, `Visit: https://example.com/reset-form`);
});
// ❌ Sequential IDs reveal business data
// GET /api/invoices/1001 → /api/invoices/1002 → competitor knows 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);
res.status(err.statusCode || 500).json({
error: process.env.NODE_ENV === 'production' ? 'Internal error' : err.message,
incidentId,
});
});
// ✅ Comprehensive Express security setup:
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
app.use(helmet({
contentSecurityPolicy: {
directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'"] },
},
hsts: { maxAge: 31536000, includeSubDomains: true },
}));
app.use(cors({ origin: ['https://myapp.com'], credentials: true }));
app.use('/api/auth/', rateLimit({ windowMs: 15 * 60 * 1000, max: 10 }));
6. Vulnerable & Outdated Components
# Audit dependencies regularly:
npm audit # Check for known vulnerabilities
npm audit fix # Auto-fix where possible
npx npm-check-updates -u # Check for newer versions
7. Identification & Authentication Failures
// ✅ Strong password policy:
function validatePasswordStrength(password) {
const checks = [
{ name: 'length', test: () => password.length >= 12 },
{ name: 'uppercase', test: () => /[A-Z]/.test(password) },
{ name: 'lowercase', test: () => /[a-z]/.test(password) },
{ name: 'digit', test: () => /\d/.test(password) },
{ name: 'special', test: () => /[!@#$%^&*]/.test(password) },
];
const passed = checks.filter(c => c.test());
if (passed.length < 4) throw new WeakPasswordError('Too weak', passed);
return passed.length / checks.length;
}
// ✅ Secure session configuration:
app.use(session({
secret: crypto.randomBytes(32).toString('hex'),
cookie: { secure: true, httpOnly: true, sameSite: 'strict', maxAge: 3600000 },
}));
// Regenerate session after login (prevent session fixation):
app.post('/api/login', async (req, res) => {
// ... authenticate ...
req.session.regenerate(() => {
req.session.userId = user.id;
res.json({ success: true });
});
});
8. Software & Data Integrity Failures
<!-- ✅ Subresource Integrity hashes -->
<script src="https://cdn.example.com/jquery.min.js"
integrity="sha256-xxxhashhere"
crossorigin="anonymous"></script>
9. Security Logging & Monitoring Failures
// ✅ Log EVERY security-relevant event:
function logSecurityEvent(event, details) {
logger.warn('SECURITY_EVENT', {
event, ...details,
timestamp: new Date().toISOString(),
ip: details.req?.ip,
userId: details.user?.id,
});
}
// When to log:
logSecurityEvent('LOGIN_SUCCESS', { req, user });
logSecurityEvent('LOGIN_FAILURE', { req, reason: 'bad_password' });
logSecurityEvent('PERMISSION_DENIED', { req, resource, action });
logSecurityEvent('RATE_LIMIT_EXCEEDED', { req, endpoint });
10. Server-Side Request Forgery (SSRF)
// ❌ Allowing user-controlled URLs in backend requests:
app.get('/api/fetch-url', async (req, res) => {
const data = await fetch(req.query.url); // User controls URL!
});
// ✅ SSRF protection:
async function safeFetch(urlString) {
let url;
try { url = new URL(urlString); } catch { throw new Error('Invalid URL'); }
const blockedPatterns = [/^127\./, /^10\./, /^172\.(1[6-9]|2\d|3[01])\./, /^192\.168\./];
const addresses = await dns.promises.resolve(url.hostname);
for (const addr of addresses) {
if (blockedPatterns.some(p => p.test(addr))) throw new Error('Blocked: internal address');
}
if (!['http:', 'https:'].includes(url.protocol)) throw new Error('Protocol not allowed');
return fetch(url, { timeout: 10000 });
}
Which OWASP vulnerability have you encountered most? What's your security tip?
Follow @armorbreak for more practical developer guides.
Top comments (0)