Skip to the solution
HardBackend6 min read

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

317 views

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.

  • nodejs
  • security
  • prototype-pollution
  • validation

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

6 min read Β· 8 sections

Read the code

JavaScriptA 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

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 147 of 152 in Node.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Node.js questions
Up nextHard

What is the role of `libuv` in Node.js?

Continue