Concept
The beginner framing: a Node.js server that trusts anything coming from a client request, the body, headers, query params, even a filename, without validating it first is exposing itself to a wide range of attacks, several of which are specific to how JavaScript and Node's APIs work.
The precise mental model: security in a Node.js application is about minimizing what untrusted input is able to cause, whether that's running arbitrary code, reading files it shouldn't, corrupting shared object state, or degrading service through resource exhaustion. Several of these have distinctly Node/JS-flavored forms.
Prototype pollution
// A naive deep-merge function, given attacker-controlled input:
function merge(target, source) {
for (const key in source) {
if (typeof source[key] === "object") {
target[key] = merge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, malicious);
const anyObject = {};
console.log(anyObject.isAdmin); // true, EVERY object in the process now has this!Because JavaScript objects inherit from Object.prototype by default, and __proto__ is a live accessor for an object's prototype, a naive merge/clone function that doesn't guard against the literal key "__proto__" (or "constructor"/"prototype") lets attacker-controlled JSON silently mutate the prototype shared by every plain object in the entire process, a uniquely JavaScript-flavored vulnerability class.
ReDoS: catastrophic regex backtracking
const evilRegex = /^(a+)+$/;
evilRegex.test("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!");
// hangs for an extremely long time, the SINGLE main thread is blocked entirelyCertain regex patterns with nested/overlapping quantifiers can exhibit exponential backtracking on specifically crafted input, since regex execution runs synchronously on the single main thread (see Runtime Overview), a single malicious request with a ReDoS-triggering string can block the entire server, affecting every concurrent request, not just that one.
The Permission Model, confirmed still experimental
node --experimental-permission --allow-fs-read=/app/data server.jsNode has an opt-in Permission Model designed to restrict what a process can do, limiting file system access, network access, or child process spawning to an explicit allowlist. Confirmed directly against this app's installed Node v23.11.0: this remains experimental (the --experimental-permission alias is present alongside --permission, and it isn't presented as stable). It's a promising sandboxing layer to be aware of, but not yet something to rely on as a primary, stable line of defense in production.
Try It
Predict the vulnerability before checking the solution.
const path = require("path");
const BASE_DIR = "/app/uploads";