Node.js Security in 2026: What Actually Matters (and What Doesn’t)

Supply chain attacks are the main Node.js threat in 2026, so I start with dependencies: run npm audit, use npm ci with a committed lockfile, and disable install scripts where possible. Your own code is usually fine. The 400 packages under it are not.
Part of the complete web development guide.
On this page
- Start With Dependencies, Because That’s Where You’ll Get Hit
- Input Validation Is Still the Whole Game
- Prototype Pollution Is Quietly Common
- Authentication: Stop Rolling Your Own Sessions
- The Server-Level Stuff People Forget
- Set a Content Security Policy
- Rate Limit by Identity, Not Just IP
- Run the Process as a Non-Root User
- What I’d Actually Do First
- FAQ
- Is npm audit enough to catch supply chain attacks?
- Should I use JWT or server sessions in 2026?
- How often should I update dependencies?
Most Node.js security advice you find online is a list of twenty things, half of which stopped mattering years ago. The real attack surface in 2026 is narrower than people think, but the mistakes are more expensive because everything is connected. Here’s what I actually check when I audit a Node app, in the order I check it.
Start With Dependencies, Because That’s Where You’ll Get Hit
Your own code is probably fine. The 400 packages under it are not. Supply chain attacks are the dominant Node.js threat right now, and they don’t look like the old “typosquatted package” stories. They look like a maintainer’s npm token getting phished, or a package adding a postinstall script that phones home during your CI build.
First thing: run npm audit and actually read it. Not the summary line. The details. Then set up automated scanning that fails your build on high and critical findings.
# Check what you actually ship, not just what's in package.json
npm audit --omit=dev --audit-level=high
# See the full dependency tree, including transitive deps
npm ls --all --depth=10
# Lockfile integrity: make sure CI uses the lockfile, never ignores it
npm ci # not npm install
Three habits that matter more than any scanner:
- Commit your lockfile and use
npm ciin CI.npm installcan silently resolve to a different version than what you tested. - Disable lifecycle scripts where you can. npm 11+ supports
--ignore-scripts, and it blocks a whole class of postinstall attacks. - Pin your direct dependencies and review every new one. If a package has one maintainer, no tests, and was published last week, you don’t need it.
Input Validation Is Still the Whole Game
Injection doesn’t go away, it just changes shape. NoSQL injection, prototype pollution, and template injection are the 2026 versions of the same old problem: you trusted user input.
If you’re on MongoDB, remember that query operators are objects. Passing a raw request body into a query is how {"$gt": ""} ends up logging someone in.
// BAD: attacker sends { "email": { "$gt": "" }, "password": { "$gt": "" } }
const user = await db.collection('users').findOne(req.body);
// GOOD: validate and coerce the shape first
import { z } from 'zod';
const LoginSchema = z.object({
email: z.string().email().max(254),
password: z.string().min(12).max(128),
});
const parsed = LoginSchema.safeParse(req.body);
if (!parsed.success) return res.status(400).json({ error: 'Invalid input' });
const user = await db.collection('users').findOne({
email: parsed.data.email,
passwordHash: await hash(parsed.data.password),
});
Validate at the boundary, once, and pass typed data inward. If you’re doing this by hand in every route handler, you’re going to miss one, and the one you miss will be the one that matters.
Prototype Pollution Is Quietly Common
Any code that merges user-controlled objects into an existing object is a candidate. Recursive merge functions from older utility libraries are the usual culprit. Modern versions of lodash, and any library that uses Object.create(null) internally, are safe. If you wrote your own deep merge, check it against __proto__, constructor, and prototype keys.
Authentication: Stop Rolling Your Own Sessions
I see more broken auth than broken crypto, and it’s always the same pattern: someone built a session system from scratch because a library felt heavy. Use a maintained library. If you’re doing JWT, the mistakes are predictable:
- Not pinning the algorithm.
jwt.verify(token, secret)withoutalgorithms: ['HS256']has bitten people for a decade. - Long-lived tokens with no revocation path. If you can’t log someone out server-side, you don’t have real auth.
- Storing tokens in
localStorage. Any XSS becomes full account takeover. Use httpOnly, Secure, SameSite cookies.
import jwt from 'jsonwebtoken';
// Verify with an explicit algorithm allowlist
const payload = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'],
issuer: 'your-app',
audience: 'your-api',
clockTolerance: 5,
});
For passwords, Argon2id is the current default recommendation. bcrypt is still acceptable if you’re already on it, but bump the cost factor as hardware gets faster. And always use a constant-time comparison for anything token-like, because === leaks timing information.
The Server-Level Stuff People Forget
Application code gets all the attention, but a surprising amount of real-world damage happens at the HTTP layer.
Set a Content Security Policy
CSP is the single highest-use header you can add. It’s also the one people skip because it’s annoying to get right. Start in report-only mode, watch the violations for a week, then enforce.
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"], // no 'unsafe-inline', ever
objectSrc: ["'none'"],
baseUri: ["'self'"],
frameAncestors: ["'none'"],
},
},
crossOriginEmbedderPolicy: true,
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
}));
Rate Limit by Identity, Not Just IP
IP-based rate limiting is trivially bypassed with a proxy pool. Limit on the account or API key for authenticated routes, and on IP only for login and signup where there’s no identity yet. Add a small delay on failed logins and log the attempts. You don’t need a WAF to stop credential stuffing, you need a counter.
Run the Process as a Non-Root User
This is boring and it’s the difference between a contained incident and a bad week. In Docker, create a user, drop to it, and mount your filesystem read-only where you can.
FROM node:22-alpine
WORKDIR /app
COPY --chown=node:node . .
RUN npm ci --omit=dev --ignore-scripts
USER node
EXPOSE 3000
CMD ["node", "server.js"]
What I’d Actually Do First
If you have a Node app in production and you’re not sure where you stand, do these in order:
- Run
npm audit --omit=devand fix anything critical today. - Grep your codebase for
jwt.verifyand confirm every call pins the algorithm. - Check that your session cookies are httpOnly and Secure, and that you have a server-side revocation path.
- Add helmet with a real CSP, starting in report-only.
- Confirm your container isn’t running as root.
That list takes an afternoon and closes most of the realistic gaps. The rest is maintenance, and maintenance is easier when the app is built on a foundation that doesn’t fight you. If you’re working with a codebase that’s grown organically and needs a security pass plus some structural cleanup, custom web development work like that is most of what I do. I’m based on Long Island, so if you’re local, we can also just grab coffee and look at the code together.
FAQ
Is npm audit enough to catch supply chain attacks?
No. It catches known vulnerabilities in published versions. It won’t catch a malicious new release that hasn’t been flagged yet. Pair it with lockfile pinning, --ignore-scripts, and a policy of reviewing new dependencies before you add them.
Should I use JWT or server sessions in 2026?
For a normal web app, server sessions with httpOnly cookies are simpler and safer. JWTs make sense for stateless APIs called by multiple services, or when you genuinely can’t share session state. If you pick JWT, plan for revocation from day one.
How often should I update dependencies?
Patch and minor updates continuously through automation you trust. Major versions on a schedule, maybe quarterly, so you’re never three versions behind on something security-relevant. The teams that get burned are the ones that let it slide for a year and then have to do it all at once.


