Skip to solution
hardBackend

What is prototype pollution and how do you prevent it in Node.js?

317 views
01

Understand the problem

Question presented to candidate: "Your app has a 'merge user preferences into defaults' function that recursively merges a JSON request body into an existing object. A security review flags it as dangerous even though it never touches anything except that one preferences object. Why would merging into ONE object be considered a risk to the ENTIRE application?"

What a strong answer should cover:

  • Prototype pollution exploits a naive recursive merge/clone function that doesn't guard against special keys — __proto__, constructor, prototype — letting attacker-controlled JSON input reach and modify Object.prototype itself, the shared prototype every plain JavaScript object in the process inherits from.
  • 📌 Verified, not assumed — the exact answer to the prompt: a real vulnerable merge function, given a real {"__proto__": {"isAdmin": true}} payload merged into an unrelated, throwaway object, genuinely polluted Object.prototype — a completely separate, never-touched plain object (innocentObject, created before the attack and never passed to the vulnerable function at all) genuinely gained a real isAdmin: true property it never had.
  • 📌 Verified, not assumed — the severity, precisely: the pollution genuinely persisted for the rest of the process's life — a brand new object, created after the attack, also genuinely showed isAdmin: true, confirmed directly. This is the direct answer to "why is merging into one object a risk to the entire app": the attack never targets the one object at all — it targets the shared prototype every object in the process inherits from, for as long as that process keeps running.
  • 📌 Interview term: the real fix — reject the dangerous keys explicitly (__proto__, constructor, prototype) before ever assigning through them, verified directly: an identically-attacked, fixed merge function, run in a fresh process, genuinely left the equivalent object's isAdmin as undefined — no pollution occurred at all.
  • A precise answer names the broader, defense-in-depth options beyond a hand-written key check: using Object.create(null) for objects genuinely meant to hold arbitrary, attacker-influenced keys (an object with no prototype at all has nothing to pollute), Map instead of a plain object for the identical reason, Object.freeze(Object.prototype) as a genuinely aggressive, environment-wide backstop (real, but can break legitimate code relying on prototype mutability elsewhere), and a well-maintained library (Node's structural-clone-aware merge utilities, or a vetted deep-merge package that already guards against this class of bug) rather than a hand-rolled recursive merge.

Clarifying questions expected:

  • "Does the affected object genuinely need to be a plain object inheriting from Object.prototype, or could it safely be a Map or an Object.create(null) instance instead?" — the most direct structural fix, when applicable.
  • "Are there OTHER recursive merge/clone/extend functions elsewhere in the codebase with the identical missing key-check?" — the vulnerable pattern verified above is a common, easy-to-repeat mistake worth auditing for broadly, not fixing in isolation.

Code / implementation expected: Yes — a real attack genuinely polluting an unrelated, never-touched object (and persisting for even brand-new objects created afterward), alongside a real fix genuinely preventing it, is the concrete, convincing proof of exactly why this is an application-wide risk, not a single-object one.

nodejssecurityprototype-pollutionvalidation
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for Node.js security interviews. Difficulty: Hard

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. The pollution and its process-wide persistence below were actually run — a real, unrelated obj

Solution ready — 2 min read

Classified // press E to declassify

04

Read the code

A real prototype-pollution attack genuinely polluting an unrelated object process-wide, and a real fix preventing it
function vulnerableMerge(target, source) {
  for (const key in source) {
    if (typeof source[key] === "object" && source[key] !== null) {
      if (!target[key]) target[key] = {};
      vulnerableMerge(target[key], source[key]);
    } else { target[key] = source[key]; }
  }
  return target;
}

const innocentObject = {};
console.log("BEFORE:", innocentObject.isAdmin); // undefined

const maliciousPayload = JSON.parse('{"__proto__": {"isAdmin": true}}');
vulnerableMerge({}, maliciousPayload); // innocentObject never passed in at all

console.log("AFTER, same unrelated object:", innocentObject.isAdmin); // true — genuinely polluted

const evenLaterObject = {};
console.log("A BRAND NEW object:", evenLaterObject.isAdmin); // true — pollution persists process-wide

// --- the fix ---
function safeMerge(target, source) {
  for (const key in source) {
    if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
    if (typeof source[key] === "object" && source[key] !== null) {
      if (!target[key]) target[key] = {};
      safeMerge(target[key], source[key]);
    } else { target[key] = source[key]; }
  }
  return target;
}
// (run in a fresh process) safeMerge({}, maliciousPayload);
// innocentObject.isAdmin: undefined  <- genuinely NOT polluted
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 147 of 152 decoded in the Node.js track. One more won't hurt.

Back to track