The New Default. Your hub for building smart, fast, and sustainable AI software

See now
title image for a blog post on Node.js security best practices

Node.js Security Best Practices: A Practical Guide to Safe Applications

Zura Iashvili
|   Updated Oct 25, 2026

Node.js security best practices come down to a short list of controls your team has to add itself: a locked-down npm supply chain, strong authentication, limits on what a single request can cost you, input validation, and hardened runtime settings.

Node.js and minimalist frameworks like Express switch on almost none of this by default. In 2025, that gap got harder to ignore: the self-replicating Shai-Hulud worm compromised more than 500 npm packages, according to CISA, and OWASP's Top 10:2025 added software supply chain failures as a new category at #3.

This guide covers the controls that matter most for a production Node.js application in 2026, the tools to implement them, and the trade-offs behind each.

Executive Summary

A secure Node.js application is the result of deliberate engineering choices, not of the runtime or the framework, because Node.js trusts every line of code it runs.

The highest-value controls are running a supported LTS release, controlling what enters your npm dependency tree, hashing passwords with Argon2id or scrypt and adding multi-factor authentication, rate-limiting and size-limiting every request, and validating input before it reaches a database or a template.

None of these is expensive on its own. Skipping them is what turns a routine dependency update or a login endpoint into a breach, with the incident-response cost, compliance exposure and lost customer trust that follow.

Why are Node.js applications exposed by default?

Node.js applications are exposed by default because the runtime treats all code it executes, including every npm dependency, as trusted, and popular frameworks leave security controls to you. Express doesn't set security headers, rate-limit requests or validate input out of the box. That's a reasonable design choice for a minimal framework, but it means a new Express or NestJS service is only as secure as the middleware and habits your team adds.

The two weakest points map directly to how breaches start. In Verizon's 2025 Data Breach Investigations Report, credential abuse (22%) and exploitation of vulnerabilities (20%) were the leading initial attack vectors, and breaches involving a third party doubled to 30%. For a Node.js team, that translates to login endpoints and dependencies. A stolen npm token or a weak password policy leads to unauthorized access, which leads to a breach notification and a delayed release. In regulated sectors like FinTech or HealthTech, it also becomes a compliance problem on top of the technical one.

Which security controls does a production Node.js application need?

Most Node.js security work falls into five areas. The table below maps each common threat to the control that stops it and the Node.js tooling you can use.

Threat

Control

Node.js tooling

Malicious or hijacked npm package

Lockfiles, release cooldowns, short-lived publishing credentials

npm ci, min-release-age, trusted publishing

Leaked password database

Slow, salted password hashing

crypto.argon2 (Node.js 24.7+), crypto.scrypt

Credential stuffing and brute force

Rate limiting, MFA, breached-password checks

rate-limiter-flexible, passkeys/WebAuthn

Denial of service

Request size limits, safe regular expressions

express.json({ limit }), eslint-plugin-security

NoSQL injection and XSS

Schema validation, filter sanitization, output encoding, CSP

Mongoose sanitizeFilter, sanitize-html, helmet

Broken access control

Authorization check on every request

Route middleware or NestJS guards

How do you protect a Node.js app from npm supply-chain attacks?

You protect a Node.js app from supply-chain attacks by controlling exactly which package versions get installed, delaying brand-new releases, and treating developer and CI credentials as production secrets. September 2025 showed why. First, attackers phished a maintainer and pushed malicious versions of widely used packages like chalk and debug. A week later, the Shai-Hulud worm began stealing npm and GitHub tokens from developer machines and using them to publish infected versions of other packages.

The controls that would have limited the damage are mostly configuration:

  • Commit your lockfile and install with npm ci in CI, so builds use the exact versions you reviewed, not whatever was published an hour ago.

  • Add a release cooldown. Recent npm versions support a min-release-age setting, and pnpm offers minimumReleaseAge. A few days' delay means most hijacked releases are detected and pulled before they reach you.

  • Limit install scripts. Lifecycle scripts run arbitrary code on npm install. Running installs with --ignore-scripts and allowing only the packages that really need scripts closes a common entry point.

  • Protect your credentials. CISA's guidance after Shai-Hulud was to rotate developer credentials and require phishing-resistant MFA on GitHub and npm. If you publish packages, npm revoked all classic tokens in December 2025; trusted publishing from CI, which uses short-lived credentials, is now the recommended route.

Keep running npm audit, but know its limits. It flags known vulnerabilities in published advisories. It won't catch a malicious version released this morning, which is exactly what the 2025 attacks relied on.

How should a Node.js application store passwords and handle logins?

A Node.js application should hash passwords with Argon2id, or scrypt if Argon2id isn't available, and never store them encrypted or in plain text. That's the order the OWASP Password Storage Cheat Sheet recommends. Bcrypt is reserved for legacy systems because it truncates input at 72 bytes. You no longer need a native third-party module for this: Node.js has shipped crypto.scrypt for years, and crypto.argon2 arrived in Node.js 24.7.0 (Node.js docs). Use the asynchronous versions. A synchronous hash blocks the event loop for every other request on that process.

Password rules have changed too. NIST SP 800-63B now says not to impose composition rules (uppercase, digits, symbols) or periodic resets. Instead, require length (at least 15 characters for password-only login, 8 when combined with MFA) and check new passwords against lists of breached or common values.

Multi-factor authentication is the single biggest upgrade to a login flow. After Google auto-enrolled more than 150 million users in two-step verification, it reported a 50% drop in compromised accounts among them. Passkeys (WebAuthn) go further, because they can't be phished. A few other login details matter:

  • Return the same generic error for a wrong username and a wrong password, so attackers can't confirm which accounts exist.

  • If you keep JWTs in the browser, put them in HttpOnly, Secure, SameSite cookies rather than localStorage, and add CSRF protection. The once-standard csurf package is deprecated, so pick a maintained alternative.

Before building any of this, ask whether you should. Unless authentication is part of your product, a managed identity provider such as Auth0, Okta, Amazon Cognito or Firebase Authentication handles MFA, passkeys and breach detection for you. The trade-off is per-user pricing and some vendor lock-in, which is usually cheaper than owning an auth system and its incident history.

How do you stop brute-force and denial-of-service attacks on a Node.js API?

You stop brute-force and denial-of-service attacks on a Node.js API by rate-limiting requests, capping request sizes, and keeping expensive work off the event loop. This matters more in Node.js than in thread-per-request runtimes: one slow, CPU-heavy request can stall every other request handled by that process.

Libraries like rate-limiter-flexible let you cap requests per IP, per user or per route. Back the limiter with Redis or another shared store once you run more than one instance; an in-memory counter on each process lets attackers multiply their allowance by your instance count. For login endpoints, limit by both IP and username, and prefer progressive delays over hard lockouts, since lockouts let an attacker lock real users out on purpose.

Since Express 4.16, express.json() and express.urlencoded() take a limit option (100 KB by default), so you don't need the separate body-parser package. Set tighter limits on routes that only expect small payloads, and stream file uploads instead of buffering them in memory.

A badly written regular expression with nested quantifiers can take seconds to evaluate a crafted string, a pattern known as ReDoS (Regular Expression Denial of Service). Keep regexes simple, cap input length before matching, and use a linter rule such as eslint-plugin-security to flag risky patterns.

How do you prevent injection and XSS attacks in Node.js?

You prevent injection and XSS in Node.js by validating every input against a schema at the edge of your application, and by encoding output for the context it's rendered in. Validation libraries like Zod or express-validator let you define what a valid request looks like and reject everything else, which beats trying to strip out known-bad input.

For MongoDB, schema validation alone isn't enough. A login query that receives { "password": { "$ne": null } } instead of a string can match any user. Turn on Mongoose's sanitizeFilter option, which wraps operator objects so they're treated as literal values, or use express-mongo-sanitize to strip $-prefixed keys. For SQL databases, use parameterized queries or your ORM's query builder, never string concatenation.

For XSS, rely on your templating engine or frontend framework's default escaping, and use sanitize-html or DOMPurify only where you must render user-supplied HTML. Older tutorials recommend xss-clean, but the package is deprecated. Add helmet to set a Content Security Policy and other security headers, which limits the damage when an injection slips through. Also keep eval(), new Function() and child_process.exec() away from anything a user controls.

Which runtime and configuration settings harden a Node.js deployment?

A hardened Node.js deployment runs a supported LTS release, keeps secrets out of the codebase, and fails safely. As of October 2026, Node.js 22 and 24 are the supported LTS lines, Node.js 26 is the Current release, and Node.js 20 no longer receives security fixes (Node.js releases). Running an end-of-life version means every new Node.js CVE stays open in your app.

The rest of the hardening checklist is short:

  1. Restrict the runtime. The Node.js Permission Model, stable since v22.13.0, limits file system, network, child process and worker access with flags like --permission and --allow-fs-read. It's a seat belt against mistakes, not a sandbox for malicious code.

  2. Keep secrets out of Git. Load API keys and database credentials from environment variables or a secrets manager, and turn on secret scanning in your repository.

  3. Use HTTPS everywhere and send an HSTS header, which helmet sets for you.

  4. Check authorization on every request, not just at login. Broken access control is #1 in the OWASP Top 10:2025.

  5. Return only the fields the client needs, so an API response doesn't leak internal IDs, password hashes or other users' data.

  6. Send generic error messages to clients and log the details server-side with a structured logger like Pino. Run Express with NODE_ENV=production so stack traces don't reach responses.

  7. Remove unused routes and endpoints, because unmaintained code is unmonitored attack surface.

Key Takeaways

  • Node.js trusts every package it runs, so dependency control is your first security layer.

  • npm audit catches known vulnerabilities, not freshly hijacked releases; use lockfiles and a release cooldown.

  • Hash passwords with Argon2id or scrypt, both built into modern Node.js; use the async APIs.

  • MFA and passkeys do more for login security than password complexity rules.

  • In Node.js, one expensive request can stall a whole process, so rate and size limits are essential.

Where should a team start with Node.js security?

Start with the controls that block the most common breach paths: a supported Node.js version, lockfile-based installs with a release cooldown, MFA and proper password hashing, and rate limits on your login and public endpoints. Then work through validation, headers and access control route by route. Security in Node.js is ongoing maintenance, so build these checks into code review and CI rather than treating them as a one-time audit.

If you'd like an experienced team to review an existing Node.js codebase or build a new web application with these controls in place from the first sprint, Monterail can help. Get in touch to talk through your application and its risks.

Node.js Security FAQ

Zurab Iashvilli avatar.
Zura Iashvili
Senior Node.js Developer
Linkedin
Senior Node.js Developer with a strong track record in backend architecture, scalable systems design, and high-impact API integrations. Specializes in building robust infrastructure that supports growth, reliability, and clean operational workflows. Experienced with microservices, event-driven systems, and modern tooling across the Node.js ecosystem.