OWASP Top 10 for Node
The OWASP Top 10 is the industry-standard list of the most critical web application security risks, updated every few years based on real-world vulnerability data. Every developer should be able to recognise each category, understand the Node-specific manifestations, and know the standard defence. This page maps each OWASP category to concrete Node.js patterns — what it looks like in Express code and how to prevent it.
A01 — Broken Access Control
// ❌ User can access ANY record by guessing the id:
app.get('/invoices/:id', authenticate, async (req, res) => {
const invoice = await db.invoices.findById(req.params.id) // no ownership check!
res.json(invoice)
})
// ✅ Scope the query to the authenticated user:
app.get('/invoices/:id', authenticate, async (req, res) => {
const invoice = await db.invoices.findOne({
id: req.params.id,
userId: req.user.id, // ownership enforced at the query level
})
if (!invoice) return res.status(404).json({ error: 'Not found' })
res.json(invoice)
})A02 — Cryptographic Failures
Don't store passwords in plaintext — use bcrypt or argon2.
Use HTTPS everywhere — TLS/HTTPS prevents interception.
Don't use MD5/SHA-1 for passwords — they're fast; use slow password-hashing functions.
Encrypt sensitive columns at rest if your DB doesn't do it (SSN, card numbers, health data).
Use
crypto.randomBytesfor tokens, nonces, salts — neverMath.random().
A03 — Injection
// ❌ SQL injection — never interpolate user input:
const users = await db.query(`SELECT * FROM users WHERE email = '${req.body.email}'`)
// ✅ Parameterized query:
const users = await db.query('SELECT * FROM users WHERE email = $1', [req.body.email])
// ❌ NoSQL injection — never pass raw objects to Mongoose queries:
User.findOne({ email: req.body.email }) // if req.body.email = { $gt: '' } → returns all
// ✅ Validate and cast the type first:
const email = String(req.body.email)
User.findOne({ email })A04 — Insecure Design
A05 — Security Misconfiguration
// Common misconfigurations in Node/Express:
app.use(helmet()) // ✅ security headers — disabled by default
app.set('x-powered-by', false) // ✅ don't advertise Express version
process.env.NODE_ENV = 'production' // ✅ disables stack traces in errors
// ❌ Default credentials, debug endpoints left on in prod:
app.get('/debug/env', (req, res) => res.json(process.env)) // never in prod!
app.use('/admin', require('./admin')) // without authenticationA06 — Vulnerable & Outdated Components
npm audit # check for known vulnerabilities npm audit fix # auto-fix where possible npx snyk test # deeper analysis with Snyk # Also: Dependabot or Renovate bot for automated PR updates
A07 — Identification & Authentication Failures
Enforce minimum password length and check against breached-password lists.
Add rate limiting and account lockout on login endpoints.
Regenerate session IDs on login (session fixation).
Use short-lived tokens + refresh tokens, not eternal JWTs.
Implement MFA for sensitive accounts.
A08 — Software & Data Integrity Failures
A09 — Security Logging & Monitoring Failures
Log authentication events — logins, failures, password changes, token issuance.
Log authorization failures — 401 and 403 responses; repeated ones signal probing.
Include a request ID in every log entry so you can trace a user's session.
Alert on anomalies — many 401s from one IP, account takeover patterns.
Never log secrets — passwords, tokens, full credit card numbers.
A10 — Server-Side Request Forgery (SSRF)
// ❌ SSRF — user controls a URL your server fetches:
app.post('/preview', async (req, res) => {
const html = await fetch(req.body.url).then(r => r.text()) // attacker passes internal URL
res.send(html)
// Attacker can use: http://169.254.169.254/latest/meta-data/ (AWS metadata)
// http://localhost:9200/ (Elasticsearch)
// http://internal-admin-service/
})
// ✅ Allowlist permitted hosts:
const ALLOWED_HOSTS = new Set(['api.partner.com', 'cdn.yourapp.com'])
const parsed = new URL(req.body.url)
if (!ALLOWED_HOSTS.has(parsed.hostname)) {
return res.status(400).json({ error: 'URL not permitted' })
}