DEV Community

Alex Chen
Alex Chen

Posted on

Web Security: OWASP Top 10 and How to Fix Them (2026)

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

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

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

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

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

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

8. Software & Data Integrity Failures

<!-- ✅ Subresource Integrity hashes -->
<script src="https://cdn.example.com/jquery.min.js"
        integrity="sha256-xxxhashhere"
        crossorigin="anonymous"></script>
Enter fullscreen mode Exit fullscreen mode

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

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 });
}
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)