Safeguard
Dev Practices

Node.js Backend Security Checklist

A working checklist for securing a Node.js backend: dependency hygiene, input validation, secrets, HTTP headers, and the CI gates that keep regressions out.

Safeguard Research Team
Research
6 min read

Securing a Node.js backend comes down to five layers: the dependency tree you install, the input you accept, the secrets you hold, the HTTP surface you expose, and the runtime you deploy on. Most real-world Node.js incidents trace back to the first two — a vulnerable or malicious npm package, or unvalidated input reaching a query, a template, or eval-adjacent code. This checklist works through each layer in the order attackers usually do, with concrete commands and configuration you can apply today.

What Makes a Node.js Backend Different to Secure?

Three properties set a Node.js backend apart from, say, a Java or Go service.

First, the dependency surface is enormous. A typical Express application resolves hundreds of transitive packages, and npm packages can run arbitrary code at install time via postinstall scripts. Your attack surface starts at npm install, not at your first request handler.

Second, JavaScript's dynamism creates classes of bugs other runtimes mostly avoid: prototype pollution (an attacker-controlled __proto__ key mutating Object.prototype), unsafe deserialization, and regular expressions that back off catastrophically (ReDoS) — all of which have shipped as real CVEs in popular packages such as lodash (CVE-2019-10744) and minimist (CVE-2020-7598).

Third, the event loop is a single lane. One synchronous crypto call or one ReDoS-triggering regex can stall every concurrent request, which turns small bugs into denial-of-service conditions.

How Do You Lock Down the Dependency Tree?

  • Commit the lockfile and install with npm ci in CI and production. This makes builds reproducible and prevents silent version drift between environments.
  • Disable install scripts by default. Run npm ci --ignore-scripts in pipelines and allowlist the handful of native modules that genuinely need build steps.
  • Scan continuously, not at release time. npm audit is a floor, not a program: it matches versions against advisories but says nothing about whether the vulnerable function is reachable from your code. A software composition analysis tool that resolves your full graph and prioritizes by reachability turns a 400-item audit report into a short fix list.
  • Pin your Node version with a .nvmrc or the engines field, and stay on an active LTS line — security fixes for the runtime itself only land on supported release lines.
  • Review new dependencies before they merge. Package age, maintainer count, and download history catch most typosquats; scoped internal packages (@yourorg/...) with per-scope registry configuration in .npmrc shut down dependency confusion.

How Should a Node.js Backend Validate Input?

Validate at the boundary, with a schema, and reject rather than sanitize.

  • Use a schema validator such as zod, joi, or ajv on every request body, query string, and header your handlers read. Coerce types explicitly; never pass req.body straight into business logic.
  • Parameterize every database query. For SQL, that means placeholders via your driver or an ORM — never string concatenation. For MongoDB, disable operator injection by validating that user input contains no $-prefixed keys, or use mongo-sanitize.
  • Guard against prototype pollution: freeze critical prototypes where practical, prefer Object.create(null) for lookup maps, and validate JSON input schemas so unexpected keys are rejected.
  • Cap payload sizes (express.json accepts a limit option) and put timeouts on any regex that processes user input, or vet patterns with a ReDoS checker.

Static analysis earns its keep here. A SAST engine tuned for JavaScript will flag tainted flows from req objects into queries, shell commands, and child_process calls long before a pentester does.

Where Do Secrets and Sessions Go Wrong?

  • Keep secrets out of the repository and out of NODE_ENV-style config files. Use your platform's secret store and inject at runtime; scan commits with a secrets detector so a leaked key fails the build.
  • Hash passwords with bcrypt, scrypt, or argon2 — never a bare SHA-256.
  • For JWTs: pin the algorithm server-side (reject alg: none and unexpected algorithm switches), keep expiry short, and validate aud and iss claims.
  • Set session cookies with httpOnly, secure, and sameSite attributes. Express's default in-memory session store is not production-safe; back sessions with Redis or a database.
  • Rotate any credential that has ever touched a log line. Structured loggers should redact token, password, and authorization fields by default.

Which HTTP-Layer Defenses Are Non-Negotiable?

  • Security headers: helmet sets sane defaults for Content-Security-Policy, Strict-Transport-Security, X-Content-Type-Options, and frame protections in one middleware.
  • Rate limiting and brute-force protection on authentication and expensive endpoints (express-rate-limit or a gateway-level equivalent).
  • CORS with an explicit origin allowlist. A wildcard origin combined with credentials is an account-takeover primitive, not a convenience.
  • Disable x-powered-by and keep error responses generic; stack traces belong in logs, not response bodies.
  • TLS everywhere, including service-to-service traffic inside your network.

How Do You Keep the Checklist Enforced Over Time?

A checklist that lives in a wiki decays in a quarter. Wire it into the pipeline instead:

  1. PR gate: lint rules (eslint-plugin-security), SAST on the diff, and dependency review on any lockfile change.
  2. Build gate: npm ci --ignore-scripts, SCA scan with a severity threshold, secrets scan.
  3. Runtime: run the container as a non-root user, drop capabilities, set NODE_ENV=production, and monitor for outbound connections your service has no business making.
  4. Recurring: patch cadence SLAs by severity, and a quarterly review of unmaintained dependencies.

Safeguard covers the dependency and code layers of this checklist — SCA with reachability analysis, JavaScript SAST, and auto-fix pull requests — if you want the gates without assembling five separate tools. For deeper background on any layer, the free courses in the Safeguard Academy walk through Node.js attack classes with runnable examples.

FAQ

Is npm audit enough to secure a Node.js backend?

No. It only matches installed versions against public advisories, so it misses malicious packages, misconfigurations, and your own code's flaws — and it over-reports vulnerabilities in code paths you never execute. Treat it as one signal inside a broader SCA and SAST program.

What is the single highest-impact item on this checklist?

Lockfile-enforced installs plus continuous dependency scanning. The npm supply chain is where most Node.js compromises start, and reproducible installs with automated scanning close the widest gap for the least effort.

Should a Node.js backend run as root in a container?

Never in production. Create a dedicated user in the Dockerfile (official node images ship a node user), drop Linux capabilities, and use a read-only root filesystem where the app allows it. Container escape bugs are rare; over-privileged containers that make small bugs catastrophic are common.

How often should dependencies be updated?

Automate minor and patch bumps weekly with Dependabot or Renovate behind your test suite, and treat critical security advisories as same-week work. Big-bang quarterly upgrades produce riskier diffs and longer exposure windows than a steady weekly cadence.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.