Skip to main content

Cloudsoft Zone Build, deploy and scale with practical guides to PHP, Node.js, React, DevOps and AI automation. Explore the guides

Cloudsoft Zone
Node.jsResource guide

Node.js production security checklist for 2026

A practical security checklist for Node.js APIs and web apps: supported versions, npm supply-chain controls, secrets, HTTP timeouts, prototype pollution, constant-time comparisons and the Permission Model.

Cloudsoft Zone

8 min read

A padlock securing a rusty iron hasp on the weathered red wooden door of an old storehouse
Photograph: Estormiz / CC0, via Wikimedia Commons
In this article

Most Node.js security incidents in small companies are not exotic. They come from an end-of-life runtime that stopped getting patches, a compromised npm package, a secret committed to Git, or an HTTP server left with defaults that let slow clients tie up every connection. None of these need a security team to fix; they need a checklist that someone works through before each release.

This guide is that checklist. It is organised from the runtime outwards: the Node.js version, dependencies, configuration and secrets, the HTTP server, your own code, and the process itself. Each item says what to do and why, with commands and code you can copy. Recommendations follow the official Node.js security best practices page, the Node.js 24 API documentation and the npm CLI documentation, all checked on 7 October 2026.

It is written for teams running Node.js APIs or web apps on a VPS, a container platform or a managed cloud service. Front-end build tooling and browser security headers are outside its scope.

1. Runtime: run a supported version and patch it

  • Use a supported release line. On 7 October 2026 the supported lines are 22.x (maintenance until 30 April 2027), 24.x (LTS until 30 April 2028) and 26.x (Current, LTS from 28 October 2026). Node.js 20 reached end of life on 30 April 2026 and gets no security releases.
  • Apply patch releases quickly. Security fixes ship as ordinary patch or minor releases on each supported line. Watch the security release posts on the Node.js blog and rebuild your images as soon as a fix lands on the line you run.
  • Pin the version everywhere it is set: Dockerfiles, CI, .nvmrc, platform runtime settings. Drift between environments hides problems until production.
A technician crouching with a laptop while working on cabling inside an open server rack
Keep the Node.js runtime on every server on a supported line and apply security releases promptly.

2. Dependencies: treat npm packages as code you run

The Node.js threat model treats the code you ask it to run, including every dependency, as trusted. A malicious package therefore runs with the same access as your application: network, file system, environment variables. The Node.js security page lists typosquatting, lockfile poisoning, compromised maintainers, malicious packages and dependency confusion as the main attack routes.

  • Commit a lockfile and install with npm ci, not npm install, in CI and builds. npm ci fails when package.json and the lockfile disagree instead of silently resolving new versions.
  • Disable install scripts with --ignore-scripts or ignore-scripts=true in .npmrc, and allow them only for packages that genuinely need a build step.
  • Add a release-age cooldown. npm 11.10.0 and newer support min-release-age, measured in days, so freshly published versions are not installed immediately. The Node.js guidance notes that most compromised packages are detected and removed within hours, so even one day removes much of that exposure. Override it per command when you need an urgent fix: npm install some-package --min-release-age=0.
  • Audit automatically. Run npm audit in CI and verify registry signatures with npm audit signatures.
  • Review new dependencies before adding them: check the exact package name for typos, the maintainers, recent publish history and what the package does at install time.
Shell
# CI: install exactly what the lockfile says, without lifecycle scripts
npm ci --ignore-scripts

# report known vulnerabilities, failing on high or critical
npm audit --audit-level=high

# verify registry signatures of installed packages
npm audit signatures
INI
# .npmrc (npm 11.10.0 or newer for min-release-age)
ignore-scripts=true
min-release-age=1

3. Configuration and secrets

  • Keep secrets out of Git. Use environment variables or your platform's secret store. Commit only an example file with placeholder values.
  • Use the built-in --env-file flag for local and simple deployments instead of an extra package. It became stable in Node.js 24.10.0. Values already present in the real environment take precedence over the file, and --env-file-if-exists skips a missing file without an error.
  • Do not log secrets. Redact authorisation headers, cookies and tokens in request logging. Make sure error responses never include stack traces or environment values.
  • Set NODE_ENV=production in every deployed environment, staging included. The Node.js documentation calls branching on other values an antipattern, because it makes staging behave differently from production.
Shell
# load configuration from a file that is never committed
node --env-file=.env server.js

4. HTTP server: timeouts, limits and the proxy

The Node.js security page lists slow-request denial of service (CWE-400) and HTTP request smuggling (CWE-444) among the main threats to HTTP servers. The mitigations are configuration, not code.

  • Run behind a reverse proxy or load balancer that terminates TLS and can rate-limit and block abusive clients.
  • Set server timeouts deliberately. In Node.js 24 the defaults are 300 seconds for requestTimeout, the smaller of 60 seconds or requestTimeout for headersTimeout, and 5 seconds for keepAliveTimeout. Most APIs can use much shorter request timeouts.
  • Bound everything you buffer: request bodies, uploads, query results. The default maximum header size is 16 KiB; leave it alone unless you have a reason.
  • Limit sockets per host and in total for outgoing requests (agent.maxSockets, agent.maxTotalSockets) and requests per keep-alive socket on the server (server.maxRequestsPerSocket).
JavaScript
import { createServer } from 'node:http';

const server = createServer((req, res) => {
  res.end('ok');
});

// Defaults in Node.js 24: requestTimeout 300000 ms, headersTimeout
// min(requestTimeout, 60000) ms, keepAliveTimeout 5000 ms.
server.requestTimeout = 30_000;    // whole request must arrive within 30 s
server.headersTimeout = 10_000;    // headers within 10 s
server.keepAliveTimeout = 5_000;   // idle keep-alive sockets close after 5 s
server.maxRequestsPerSocket = 1000;

server.listen(3000);

If your proxy keeps idle connections to Node.js open longer than keepAliveTimeout, it can send a request on a socket that Node.js is closing. Make the proxy's upstream idle timeout shorter than the Node.js value.

Long aisle between white perforated server cabinets in a brightly lit data centre in Amravati, India
Run Node.js behind a reverse proxy or load balancer, and restrict network access at the infrastructure level.

5. Application code

Validate input and guard against prototype pollution

Prototype pollution (CWE-1321) happens when untrusted keys such as __proto__ are merged into objects. The Node.js guidance recommends schema validation of external requests, objects created with Object.create(null), checking own properties with Object.hasOwn(), avoiding unsafe recursive merges, and optionally the --disable-proto flag.

JavaScript
// validate untrusted keys before merging, and check own properties only
const allowed = new Set(['name', 'email', 'plan']);
const update = Object.create(null);
for (const [key, value] of Object.entries(JSON.parse(body))) {
  if (allowed.has(key)) update[key] = value;
}
if (Object.hasOwn(update, 'plan')) { /* ... */ }

Compare secrets in constant time

Comparing tokens or signatures with === can leak information through response timing (CWE-208). Use crypto.timingSafeEqual(), which requires equal-length buffers; hashing both values first satisfies that. For stored passwords, use a password hashing function such as crypto.scrypt() rather than comparing values at all.

JavaScript
import { createHash, timingSafeEqual } from 'node:crypto';

// Compare a presented API token with the expected one in constant time.
// Hashing first gives both buffers the same length, which timingSafeEqual requires.
export function tokensMatch(presented, expected) {
  const a = createHash('sha256').update(presented).digest();
  const b = createHash('sha256').update(expected).digest();
  return timingSafeEqual(a, b);
}

Other code-level items

  • Use parameterised queries or your ORM's query builder; never build SQL from strings.
  • Never pass user input to child_process.exec(); use execFile() or spawn() with an argument array if you must run commands.
  • Handle 'error' events and promise rejections so a bad request cannot crash the process.
  • Set security headers and cookie flags (HttpOnly, Secure, SameSite) in your framework or proxy.

6. The process: least privilege

  • Run as a non-root user in containers and on servers, with a read-only file system where possible.
  • Consider the Permission Model. Stable since Node.js 22.13.0 and 23.5.0, --permission denies file system access, child processes, worker threads, native add-ons, WASI and the inspector unless you grant them with flags such as --allow-fs-read, --allow-fs-write, --allow-child-process and --allow-worker. Code can check grants with process.permission.has().
  • Know its limits. The model does not restrict network access, it is not inherited by worker threads, existing file descriptors bypass it, and symbolic links can lead outside granted paths. It reduces the impact of a mistake; it is not a sandbox for untrusted code. The documentation recommends OS-level isolation (separate users, containers, seccomp or AppArmor) for that.
  • Try audit mode first. Node.js 24.20.0 added --permission-audit, which reports would-be denials through node:diagnostics_channel without blocking anything, so you can discover which grants your app needs.
Shell
# deny everything the Permission Model covers, then allow what the app needs
node --permission \
  --allow-fs-read=/srv/app \
  --allow-fs-write=/srv/app/uploads \
  server.js
Shell
$ node --permission -e "require('fs').readFileSync('/etc/hosts')"
...
  code: 'ERR_ACCESS_DENIED',

The checklist

AreaCheckDone
RuntimeSupported line (22, 24 or 26); patches applied within days[ ]
RuntimeSame pinned version in Docker, CI and local tooling[ ]
DependenciesLockfile committed; npm ci in CI[ ]
DependenciesInstall scripts disabled by default; min-release-age set[ ]
Dependenciesnpm audit and npm audit signatures in CI[ ]
SecretsNo secrets in Git or logs; example env file only[ ]
ConfigNODE_ENV=production in every deployed environment[ ]
HTTPBehind a reverse proxy; TLS terminated[ ]
HTTPRequest, header and keep-alive timeouts set; body sizes bounded[ ]
CodeInput validated; no unsafe merges; timingSafeEqual for secrets[ ]
CodeParameterised queries; no exec() with user input[ ]
ProcessNon-root user; Permission Model evaluated[ ]

Next steps

Copy the table into your release template and work through it once for each service. Start with the runtime and dependency items, which take minutes and close the most common gaps, then schedule the HTTP and Permission Model work. Re-check the version lines each quarter; the next big date is 28 October 2026, when Node.js 26 becomes LTS.

Frequently asked questions

  • Does the Node.js Permission Model block network access?

    No. In Node.js 24 it covers the file system, child processes, worker threads, native add-ons, WASI, the inspector and OpenSSL store loaders. Restrict network access at the container, firewall or proxy level.

  • Is npm ci safer than npm install?

    For builds, yes. npm ci installs exactly what the lockfile specifies and fails if it does not match package.json, so unexpected versions cannot slip in.

  • What are the default HTTP server timeouts in Node.js 24?

    requestTimeout is 300000 ms, headersTimeout is the smaller of 60000 ms and requestTimeout, and keepAliveTimeout is 5000 ms.

  • Do I still need dotenv?

    Often not. The built-in --env-file flag is stable as of Node.js 24.10.0 and supports comments, quoted values and multi-line values.

Advertise on CloudSoftZone

Your next customer could start right here.

Introduce your tool, product or business to readers exploring technology.

Advertise with us

Explore placements and request a private quote

Sources

  1. Security best practices — Node.js
  2. Permissions (Node.js v24 docs) — Node.js
  3. Command-line options (Node.js v24 docs) — Node.js
  4. HTTP (Node.js v24 docs) — Node.js
  5. Node.js, the difference between development and production — Node.js
  6. npm ci — npm
  7. npm audit — npm
  8. Node.js Releases — Node.js

Written with substantial AI assistance. Facts checked against cited sources. Editorial policy.

Comments

No comments yet. Be the first to join the conversation.

Leave a comment

Every comment is read by a moderator before it appears. Your email address is never published; it is used only to contact you about this comment.

Never published. Used only to contact you about this comment.

Loading the security check…

Comments are moderated before publication.