DEV Community

Alex Chen
Alex Chen

Posted on

Web Security: OWASP Top 10 — Practical Defense Guide (2026)

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)
Enter fullscreen mode Exit fullscreen mode

#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;
}
Enter fullscreen mode Exit fullscreen mode

#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'],
});
Enter fullscreen mode Exit fullscreen mode

#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();
Enter fullscreen mode Exit fullscreen mode

#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.' },
}));
Enter fullscreen mode Exit fullscreen mode

#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
Enter fullscreen mode Exit fullscreen mode

Which OWASP vulnerability have you encountered most? What's your security tip?

Follow @armorbreak for more practical developer guides.

Top comments (0)