NodeJSOWASP Top 10 for Node

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

JS
// ❌ 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)
})
Broken access control is the #1 OWASP risk — always scope queries to the authenticated user
The most common form in Node apps: an endpoint looks up a resource by ID from the URL without checking ownership. Any authenticated user can enumerate IDs and access other users' data (an **IDOR — Insecure Direct Object Reference**). Scope every query to `req.user.id` where applicable, and write tests that verify one user cannot access another's resources. [RBAC](/nodejs/rbac) and [protecting routes](/nodejs/protecting-routes) cover the server-side enforcement patterns.
A02 — Cryptographic Failures
  • Don't store passwords in plaintext — use bcrypt or argon2.

  • Use HTTPS everywhereTLS/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.randomBytes for tokens, nonces, salts — never Math.random().

A03 — Injection

JS
// ❌ 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
Security must be designed in, not retrofitted — threat-model early
Insecure design means the architecture itself is flawed, not just the implementation. Examples: a password-reset flow that lets an attacker reset any account, a multi-tenant app where query logic can leak across tenants, or rate-limit checks that run after the expensive work. Fix: do lightweight threat modelling ("what can go wrong here?") during feature design, before writing code. The cheapest security fix is the one that never needed to be built insecurely.
A05 — Security Misconfiguration

JS
// 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 authentication
A06 — Vulnerable & Outdated Components

Bash
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
Verify integrity of dependencies and CI/CD pipeline artifacts
In Node this shows up as: untrusted npm packages, no `package-lock.json` in source control (allowing dependency version drift), and using `eval()` or `Function()` on untrusted input. Lock your dependency tree (`package-lock.json` or `yarn.lock`), use `npm ci` (not `npm install`) in CI/CD, and audit the packages you depend on — especially transitive dependencies. Supply-chain attacks via malicious npm packages are a real and growing threat.
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)

JS
// ❌ 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' })
}
Never fetch a user-supplied URL without strict allowlisting
SSRF lets an attacker make your server request internal services — the AWS instance metadata endpoint, an internal admin panel, a database with no auth on localhost. Your server has network access an external attacker doesn't. If you must fetch user-supplied URLs, validate against a strict allowlist of permitted hostnames, block private IP ranges (10.x, 172.16.x, 192.168.x, 127.x, 169.254.x), and use a DNS-rebinding-aware HTTP client. The safest default is to not fetch user-supplied URLs at all.
Next
Start fixing the easiest layer: [Securing Headers with Helmet](/nodejs/helmet).