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 modifyObject.prototypeitself, 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 pollutedObject.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 realisAdmin: trueproperty 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'sisAdminasundefined— 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),Mapinstead 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.