Node.js interview questions: Security

Security questions are rarely solved by listing middleware. Interviewers give you an apparently ordinary boundary - a redirect, file path, webhook, shell command, cookie, or proxy header - and look for the place untrusted data becomes authority. A senior answer validates the immediate input. A staff answer also maps trust boundaries, chooses the canonical representation, limits blast radius, and explains how the system behaves when credentials leak or a legitimate provider retries the same signed request.

The opening answers in this category contain advice that sounds safe in isolation. The follow-ups test whether it survives encoding tricks, alternate headers, race conditions, replay, and operational failure. Work each question by identifying the attacker-controlled bytes, the decision they influence, and the narrow server-side allowlist that replaces them. Then add observability without logging secrets. The unlocked example demonstrates how authentication, authorization, and input validation stay separate. The goal is not to recite a checklist; it is to show that every privileged action has an explicit, testable reason to trust its inputs.

Covered in Volume 6: Security deep-dive
Question 01Fully unlocked sample

When you're building a Node backend, what vulnerability classes are you actually defending against, and where does each one get into the system?

Strong answers name the vulnerability classes that actually hit Node backends and point at the exact spot each one gets in, instead of reciting OWASP.

What an AI-prepared candidate might say

So the big ones would be injection, broken auth and authorization, sensitive data exposure, and then vulnerable dependencies, that kind of thing. Injection is like SQL injection where you're concatenating strings into queries, and command injection when user input gets passed to a shell. For auth I'd do proper session or token handling with secure cookie flags, and check authorization on every request. Secrets go in environment variables, never in the repo, and traffic runs over TLS obviously. Dependencies are kind of a Node-specific problem because a typical app pulls in this huge tree of packages, so I run npm audit and keep a lockfile committed. And then the usual stuff, validate and sanitize user input, set security headers, rate limiting to slow down abuse. Basically defense in depth, so one mistake doesn't sink the whole thing.

Senior

I like naming these by mechanism, because each one has a specific door it walks in through. Command injection comes in through child_process. exec and execSync run their string through a shell, so any user input in there can carry shell metacharacters. execFile and spawn without shell: true pass arguments as an array straight to the binary, no shell involved at all, and that just deletes the whole class. Pick the array form and you're done with it.

Prototype pollution is the weird Node one. An attacker sends keys like __proto__ in a parsed JSON body or a merged query object, and that writes onto Object.prototype. Now every object in the process inherits the polluted property, and behavior changes nowhere near where the injection happened, which makes it miserable to debug. It gets in through recursive merges, Object.assign from untrusted data, sloppy query parsers.

Then there's input-driven denial of service, which comes in through the body parser and the regex engine. Deeply nested JSON, a huge body with no size limit, a regex with catastrophic backtracking, the ReDoS thing. Any of those lets a tiny request burn a ton of CPU or memory. Payload size limits, depth limits, and keeping nested quantifiers out of your regexes covers it.

And the dependency tree. Your own code is a fraction of what actually runs. Lockfiles pin versions, sure, but a compromised package or a malicious install script executes with your privileges. Last one is secrets exposure through config, credentials sitting in the repo, in logs, in error output. The point is each class maps to an actual line of code or config, so you can audit for it instead of waving at 'validate input.'

Staff

When I bring this into a design review I order it by blast radius, because you're never fixing everything at once. Some of these hand an attacker the whole box, others just degrade one endpoint, and they should not get equal attention. Remote code execution goes on top. Command injection through child_process, a malicious dependency running at install or runtime, deserializing untrusted data into something executable. Those get the hard controls. No shell interpolation anywhere, execFile and spawn with array args, install scripts reviewed or flat-out disabled in CI, lockfile with integrity hashes.

Next tier down is auth bypass and data exposure. Prototype pollution flipping an authorization flag lives here, so do secrets leaking into logs. I don't trust humans to catch these in review, I put automated gates on them. A scanner that flags exec calls with interpolation, a secret scanner in CI that actually fails the build, dependency provenance so a package from an unexpected publisher gets caught.

Below that, input-driven DoS. It hurts availability but doesn't hand anyone access, so it gets rate limits, body-size caps enforced during the read, regex review. It doesn't block a release the way an RCE finding does, and I'm comfortable saying that out loud.

The thing I'll push back on hard is a wall of npm audit warnings treated like a security program. I've watched a team drown in those, and most of them were non-exploitable transitive advisories in dev dependencies, which is exactly how the one exploitable path gets missed. Spend the review budget on the catastrophic classes, automate the rest, and tie the whole model to specific Node entry points, with gates on anything that grants code execution.

Follow-up chain

  1. Okay, of all these, which one is the most Node-specific, the one somebody coming from Java or Go probably has never seen?
  2. Walk me through that one. How does prototype pollution actually get you to remote code execution or an auth bypass?
  3. So most of your attack surface is really the dependency tree. What do you actually do about that?
  4. You can't fix all of these at once, so how do you pick what gets fixed first?
Question 02First answer included

Say you have to shell out to a command and part of it comes from user input. How do you do that without handing someone command injection?

Strong answers know exactly which child_process APIs launch a shell and defend with argument arrays instead of trying to escape strings.

What an AI-prepared candidate might say

So the danger is user input ending up inside a shell command. Like exec runs the whole command string through a shell, so if you build it with user input, something like exec(`convert ${filename}`), then a filename with shell metacharacters in it can run arbitrary commands. The safer way is spawn or execFile, where you pass the command and its arguments separately as an array, and then the input just gets treated as one argument instead of shell syntax. I'd also validate any user input, and I try not to build command strings by concatenation at all. If there's a native library that does the same job I'd probably just use that and skip the shell entirely. And run the process with least privilege, so even if something goes wrong the damage stays limited. Basically never trust user input in a command line.

Senior

Exactly where the shell-invoking APIs stop and the direct-exec ones start, why argument arrays close the hole for good, and where shell: true quietly opens it back up.

Staff

What you do when you genuinely can't avoid a shell, why allowlisting beats escaping, and how you hunt down every injection sink in a big codebase.

Follow-up chain

  1. So which of the child_process functions actually spawn a shell and which don't, and why does that one thing decide everything?
  2. Okay, say I write spawn with shell: true. What did I just give up, and is that ever acceptable?
  3. But what if you genuinely need shell features, pipes, globbing, that kind of thing? How do you keep that safe?
  4. Say you inherit a big existing codebase. How would you go find every unsafe shell-out in there?
Question 03First answer included

Walk me through how you handle secrets in a Node service. Where do they live, how do they get rotated, and what do you do when one leaks?

Strong answers treat secrets as a whole lifecycle, injection, rotation, revocation, blast radius, and go well past 'put them in .env and gitignore it'.

What an AI-prepared candidate might say

So the rule is you never hardcode secrets or commit them, database passwords, API keys, any of that. The standard thing is environment variables, so in development you'd have a .env file that's listed in .gitignore so it never gets committed. Then in production you inject them through the environment, or you use a secrets manager, like Vault or whatever secret store your cloud provider has. You rotate secrets periodically, and immediately if one gets exposed. Access should be least privilege, so only the services that actually need a secret can read it. You also want to make sure you're not logging secrets by accident, since logs can leak them. If one is compromised you rotate it and revoke the old one. A proper secrets manager gives you centralized control, auditing, and rotation support instead of credentials scattered across config files.

Senior

Where secrets actually enter the process, why .env belongs in local development and nowhere near production, and what rotation demands from the app itself.

Staff

The leak runbook, revoke first, then rotate, then audit the blast radius, plus how you design so a rotation never forces the whole fleet to restart at once.

Follow-up chain

  1. Okay, a secret just got committed and pushed. Deleting the file isn't enough, so what do you actually do?
  2. And why doesn't rewriting the git history cover it on its own?
  3. So how does a long-running Node process pick up a rotated database password without you restarting everything at once?
  4. What's actually wrong with reading secrets from a .env file in production, and what would you use instead?
Question 04First answer included

Most of the code actually running in your Node service is stuff you didn't write. How do you manage that supply-chain risk?

Strong answers get concrete about lockfile integrity, install scripts, and provenance, and treat npm audit as noisy triage instead of a release gate.

What an AI-prepared candidate might say

Yeah, so in Node most of your code comes from dependencies, so this matters a lot. The basics would be, commit a lockfile so everyone installs the same versions, and use npm ci in CI so installs are reproducible. Run npm audit to find known vulnerabilities in your dependencies and update the affected packages. You want to keep dependencies fairly current, but review the updates instead of just upgrading blindly. And be careful adding new ones in the first place, prefer well-maintained, widely used packages and keep the count small, since each one is code you're trusting. There's also typosquatting, where a malicious package sits on a name really close to a popular one. Tools like Dependabot or Snyk can automate the vulnerability detection part. Basically the goal is to reduce and monitor how much untrusted third-party code is running in your service.

Senior

What the lockfile and its integrity hashes really guarantee, why install scripts are the scariest part of the whole chain, and the point where npm audit stops helping.

Staff

A supply-chain posture you can actually defend, pinning, install-script control, provenance, small trees, and how to triage a wall of audit warnings without upgrading blind.

Follow-up chain

  1. So a postinstall script runs arbitrary code the moment you npm install. What does that mean for your CI, and what do you do about it?
  2. And where does npm ci differ from npm install here? Why does that matter for reproducibility?
  3. Say npm audit comes back with 40 vulnerabilities. Why is that number nearly useless, and how would you triage it?
  4. Those integrity hashes in package-lock.json, what do they actually protect you from, and what don't they?
Question 05First answer included

You've got two internal Node services talking to each other. How do you secure that traffic, and what does mutual TLS actually buy you over regular one-way TLS?

Strong answers treat service-to-service TLS as an identity problem, chain validation, mTLS, rotation, and go well past 'just encrypt the traffic'.

What an AI-prepared candidate might say

So internal traffic should still run over TLS, so nobody on the network can read it or tamper with it. Regular one-way TLS is the server proving its identity to the client with a certificate signed by a trusted certificate authority. Mutual TLS, mTLS, goes one step further, both the client and the server present certificates, so each side verifies who the other one is. That's useful for internal services where you only want authorized callers reaching each other, not just anyone who can hit the network. You'd manage the certificates through a certificate authority, usually an internal one for internal services, and rotate them before they expire. So basically mTLS gets you mutual authentication, a service only accepts connections from clients presenting a valid certificate, which is stronger than just trusting network location.

Senior

What one-way TLS actually authenticates, what mTLS adds on top, how chain validation decides trust, and why a self-signed internal cert can be perfectly fine.

Staff

What running mTLS in production really takes, cert distribution, rotation without downtime, revocation that works, and what breaks when a chain or a clock is wrong.

Follow-up chain

  1. So in one-way TLS the client checks the server. What does mTLS add on top, and what attack does that actually stop?
  2. And on the server side, how does it actually decide to trust the client's certificate?
  3. Certs expire eventually. How do you rotate an internal CA or a leaf cert without taking an outage?
  4. One day a service starts rejecting a peer with a certificate error. What do you check first, what's most likely?
Question 06First answer included

For API auth, sessions or JWTs, how do you pick between them, and what do people usually get wrong about JWTs?

Strong answers weigh sessions against JWTs by what each one can actually do, with revocation front and center.

What an AI-prepared candidate might say

So sessions keep the state on the server. You log in, the server creates a session and sends back a session ID in a cookie, and then it looks that session up on every request. JWTs are the stateless version, the token itself carries the user's claims, signed by the server, so it can verify it without a lookup. A lot of APIs and microservice setups like JWTs because they scale without a shared session store. The catch is JWTs are harder to revoke, they stay valid until they expire. For cookies you want HttpOnly so JavaScript can't read them, Secure so they only travel over HTTPS, and SameSite to protect against CSRF. And keep token lifetimes short, with refresh tokens for longer sessions. It kind of depends on your architecture, but plenty of APIs run JWTs for the statelessness and just stay careful about storage and expiry.

Senior

Where a session and a JWT each keep their state, why revoking one is trivial and the other is a fight, and the cookie flags that decide if either is safe to ship.

Staff

The revocation and token-lifetime setup you'd actually defend in production, why localStorage is an XSS liability, and what you watch to keep token risk honest.

Follow-up chain

  1. So the whole pitch for JWTs is stateless, no session store. What does that cost you the day you need to revoke one?
  2. Is there a pattern that keeps most of the JWT benefit but gets you revocation back?
  3. Where would you actually store the token in a browser, and why does everyone say HttpOnly cookie instead of localStorage?
  4. Which cookie flags actually matter on an auth cookie, and what is each one there to stop?
Question 07First answer included

Picture a request that's completely well-formed, no injection anywhere. How can the input itself still take your Node service down?

Strong answers treat input validation as a DoS surface, payload size, nesting depth, regex backtracking, on top of the usual correctness and injection checks.

What an AI-prepared candidate might say

So valid input can still be a denial of service if it just makes the server do too much work. There's the JSON bomb, a payload that's small to send but really expensive to parse or expand, like a deeply nested structure. And ReDoS, regular expression denial of service, where a crafted input triggers catastrophic backtracking in a badly written regex, so the match takes a very long time and the process basically hangs. The defense is limits, mostly. Set a maximum request body size so huge payloads get rejected, cap the nesting depth, and write regexes to avoid the patterns that are prone to backtracking. In Express you'd configure the body parser's size limit. Validate input against a schema, reject anything oversized or malformed early, and keep regexes with nested quantifiers away from user input. Rate limiting helps too, it caps how many expensive requests one client can send. You want to bound what any single request can consume.

Senior

The three ways plain input becomes a DoS, oversized bodies, deep nesting, catastrophic regex backtracking, and why each one hits a single-threaded runtime so much harder.

Staff

Which limits to set and where to actually enforce them, how a ReDoS shows up as event-loop lag in production, and how you catch a bad regex before it ships.

Follow-up chain

  1. Why does one expensive regex hurt a Node server so much more than the exact same regex on a thread-per-request server?
  2. And how would you catch a regex like that before it ever ships?
  3. Where exactly does the body-size limit get enforced, and why isn't checking Content-Length good enough?
  4. Okay, a deeply nested JSON object, tiny in bytes but expensive to process. What's the attack there, and what's the defense?
Question 08First answer included

Can you explain prototype pollution to me, how the attack actually works in Node, and what you'd do to prevent it?

Strong answers show the attacker-controlled key actually writing onto Object.prototype, and can name the specific defenses that stop it.

What an AI-prepared candidate might say

Prototype pollution is basically when an attacker manages to modify Object.prototype, the base object every JavaScript object inherits from. It usually comes in through user input with special keys like __proto__ or constructor.prototype. So if your code takes untrusted data and merges it into an object, like a deep merge or an unsafe Object.assign, the attacker can set properties on the prototype, and then those properties affect every object in the app. That can mean denial of service, changed application logic, or in some cases even code execution. The defense is to validate input and avoid unsafe merges of untrusted data. You can use objects with no prototype, freeze Object.prototype, block the dangerous keys like __proto__, or use a schema validator that only accepts expected fields. Keeping dependencies updated helps too, libraries have shipped these bugs before. The core idea is keeping user-controlled keys off the prototype chain.

Senior

Follows a __proto__ key from the request body all the way onto Object.prototype, why the merge is the step that breaks, and how the pollution surfaces across the whole process.

Staff

The layered defenses that actually hold up, null-prototype objects, key blocking, schema validation, the runtime flag, and how you track down the vulnerable merge in your own code.

Follow-up chain

  1. So JSON.parse happily hands you an object with a __proto__ key in it. Which line of code is the actual vulnerability then?
  2. Can you give me a defense that kills the merge problem without hunting down every single call site?
  3. Once Object.prototype is polluted, how does that end up biting code nowhere near the injection point?
  4. There's a Node runtime flag that helps with this. Why do you call it a backstop and never the actual fix?
Question 09First answer included

Say you have to run untrusted code inside a Node service, like a user's script or a plugin. How do you isolate it?

Strong answers say straight out that vm is not a security boundary, then reason about real isolation, separate processes, the permission model, OS sandboxing.

What an AI-prepared candidate might say

So Node has a vm module that runs code in a separate context, and people sometimes use it to sandbox untrusted code. But the vm module isn't a real security boundary. The Node docs actually say straight up not to use it for untrusted code, because there are known ways to escape the context and reach the host. Genuinely untrusted code needs stronger isolation than that. You'd run it in a separate process with limited privileges, or in a container, or in a dedicated sandbox, something like a microVM or a WebAssembly runtime. You limit what the code can touch, so the filesystem, the network, the environment, and you set resource limits so it can't exhaust CPU or memory. Node also has a permission model now that can restrict filesystem and network access. The safe move is basically to assume the code is hostile and isolate it at the operating system level instead of inside the same Node process.

Senior

Why a fresh V8 context contains nothing, what the vm module is really for, and how actual isolation stacks up, process separation, the permission model, OS sandboxing.

Staff

How you build an isolation boundary you'd stand behind, process separation, resource limits, the permission model, seccomp or microVMs, and what each layer actually stops.

Follow-up chain

  1. The Node docs literally say vm isn't a security mechanism. So how does code actually escape a vm context in practice?
  2. Fair enough, so what is the vm module actually good for then?
  3. What does Node's permission model actually buy you, and at what point does it stop being enough for untrusted code?
  4. Say the code you're running is genuinely hostile. What does the real isolation stack look like?
Question 10First answer included

People mix up CORS and CSRF all the time. What does each one actually do, and how do they apply to a JSON API?

Strong answers keep CORS as a browser policy and CSRF as an attack, and know which one protects a request, especially for token or cookie APIs.

What an AI-prepared candidate might say

So CORS, cross-origin resource sharing, controls which origins can make requests to your API from a browser. Browsers block cross-origin requests by default, and CORS headers let your server name the origins that are allowed. CSRF, cross-site request forgery, is an attack, where a malicious site tricks a user's browser into sending a request to your site using the session cookie the user already has. They kind of solve different problems. CORS governs which origins can read your responses, and CSRF is more about unwanted requests reusing the user's credentials. To prevent CSRF you use anti-CSRF tokens, the SameSite cookie attribute, origin checks, that sort of thing. For CORS you set the allowed origins carefully instead of pairing a wildcard with credentials. And on an API that uses token-based auth in a header instead of cookies, CSRF matters less, because the browser doesn't attach the token on its own. You want to configure both correctly.

Senior

What CORS actually relaxes and who enforces it, what CSRF really exploits, and why the two need completely different defenses.

Staff

The right posture for a JSON API depending on whether it authenticates with cookies or bearer tokens, and the misconfig that turns CORS itself into the hole.

Follow-up chain

  1. Someone tells you 'we turned on CORS, so we're covered against cross-site attacks.' What's wrong with that?
  2. Okay, so on a cookie-authenticated API, what actually stops CSRF?
  3. Why is a bearer-token-in-a-header API mostly immune to classic CSRF?
  4. How does a CORS misconfiguration end up being the vulnerability itself?

THE BASELINE GETS YOU THROUGH QUESTION ONE.

Raw Mode trains the follow-ups.

Unlock every Senior and Staff answer, every tree answer, the debug repos, hallucination drills, design scenarios, and the framework capstone.Get NodeBook Raw Mode