NodeJSSecurity Best Practices

Security Best Practices

The previous pages in this section each cover a specific threat. This page is the consolidation: the practices that span every layer, the defaults you should configure on every project, and the ongoing habits that keep a deployed application secure as it evolves. Think of it as the checklist you run before shipping and the habits you build into your development workflow.

Configuration defaults for every Express app

JS
import express from 'express'
import helmet from 'helmet'
import rateLimit from 'express-rate-limit'
import mongoSanitize from 'express-mongo-sanitize'

const app = express()

// Security headers:
app.use(helmet())

// Remove X-Powered-By (Helmet does this, but be explicit):
app.disable('x-powered-by')

// Parse bodies with a size limit — prevents request flooding:
app.use(express.json({ limit: '100kb' }))
app.use(express.urlencoded({ extended: true, limit: '100kb' }))

// Global rate limit — protect all endpoints:
app.use(rateLimit({ windowMs: 60_000, max: 200, standardHeaders: true }))

// Strip MongoDB operators from request body/params (if using Mongoose):
app.use(mongoSanitize())
Input and output discipline

Rule

Why

Validate all input with a schema

Reject bad shapes before they reach business logic

Parameterize all queries

Prevents SQL/NoSQL injection categorically

Escape output in templates

Prevents stored/reflected XSS

Sanitize rich HTML with DOMPurify

Prevents stored XSS from user-generated content

Explicit field selection in DB queries

Prevents over-fetching and accidental data exposure

Never log passwords or tokens

Prevents credential leak via log aggregation

Authentication and authorization
  • Hash passwords with bcrypt (≥10 rounds) or argon2id — never store plaintext.

  • Rate-limit and lock out login endpoints — block brute-force.

  • Short-lived tokens (15m access + refresh tokens) — limit stolen-token blast radius.

  • HttpOnly + Secure + SameSite on all session/auth cookies.

  • Deny by default — every route requires auth unless explicitly public.

  • Scope DB queries to the authenticated user — prevent IDOR.

  • Server-side authorization only — never trust client-side role checks.

Secrets and environment
  • No secrets in source control — environment variables or secrets manager only.

  • Validate required env vars at startup — fail loudly before accepting traffic.

  • Rotate secrets regularly and immediately on suspected exposure.

  • Separate secrets per environment — dev/staging/prod each have their own.

  • Use npm ci in CI/CD — installs exactly from the lockfile.

Dependencies and supply chain

Bash
# In CI — fail on high/critical vulnerabilities:
npm audit --audit-level high

# Keep dependencies current:
npm outdated

# Review new packages before adding them
Defensive coding patterns

JS
// 1. Explicit field allow-list — never spread req.body directly:
const { name, email } = req.body
await db.users.update(id, { name, email })

// 2. Always check ownership — never just findById:
const post = await db.posts.findOne({ id, authorId: req.user.id })

// 3. Avoid eval, Function(), child_process.exec with user input:
// ❌ exec(`convert ${req.body.filename}`)
// ✅ execFile('convert', [sanitizedFilename])

// 4. Set limits everywhere:
app.use(express.json({ limit: '100kb' }))  // body size
app.get('/search', (req, res) => {
  const limit = Math.min(Number(req.query.limit) || 20, 100)  // page size cap
})
Headers to verify on every deployment

Bash
curl -I https://yourapp.com | grep -E '(Content-Security|X-Frame|Strict-Transport|X-Content)'
# Content-Security-Policy: default-src 'self'; ...
# X-Frame-Options: DENY
# Strict-Transport-Security: max-age=31536000; includeSubDomains
# X-Content-Type-Options: nosniff
Ongoing security hygiene
  • Security-review new features during design — not after implementation.

  • Add auth tests for every protected route — unauthenticated, wrong role, correct role.

  • Run npm audit in CI and treat high/critical as a build failure.

  • Use Dependabot or Renovate for automated dependency update PRs.

  • Review logs for anomalies — many 401s/403s signal probing.

  • Grade your live site at securityheaders.com and SSL Labs periodically.

  • Have an incident response plan — who to notify, how to revoke, what to audit.

Security is a continuous practice — ship it and maintain it, don't set it and forget it
A security configuration that was correct at launch can drift: a dependency gets vulnerable, a new route is added without auth, a CSP is loosened for a feature and never tightened. Build security checks into your workflow: automated scanning in CI, auth tests as part of the test suite, and periodic manual review of headers, permissions, and dependency freshness. The goal isn't perfect security — it's keeping the cost of attack reliably above the value of the target.
Next
Verify your code works correctly: [Testing in Node.js](/nodejs/testing-intro).