Prototype Pollution: The Complete Practical Guide

Preventing Prototype Pollution

How to prevent prototype pollution: null-prototype maps, Map, safe merge utilities, schema validation of dangerous keys, and why freezing is not a full fix.

Published

Prototype pollution has one real fix and several supporting controls. The real fix is to never walk untrusted keys into an assignment on a pollutable object. Everything else reduces the chance a slip becomes impact. This page is the defensive reference for the cluster. Want to see why the weak controls fail? Practice the bypasses on a live target.

Use null-prototype objects for maps

Any object that holds untrusted keys should have no prototype:

const map = Object.create(null);   // no prototype -> nothing to pollute, nothing inherited
map[userKey] = value;              // a missing key returns undefined, not an inherited value

A null-prototype object closes both halves: there is no shared prototype to write onto through it, and a missing-key read does not fall through to one.

Prefer Map over plain objects

For dictionaries of untrusted keys, Map is purpose-built and immune:

const m = new Map();
m.set(userKey, value);   // keys live in the Map, not as object properties; no prototype involved

Map keys never touch Object.prototype, so __proto__ as a key is just a string.

Fix the merge, the real source

The write you control is in your own merge and clone code. Replace unsafe helpers:

  • Use merge/clone utilities that skip __proto__, constructor, and prototype (maintained libraries have patched these).
  • Never assign through attacker-controlled keys onto a normal object. Merge onto a null-prototype target.
  • Avoid shallow Object.assign of untrusted sources: an own __proto__ key re-parents the target object (a narrower hijack, not global Object.prototype pollution, but still dangerous). Spread ({...obj}) is safe because it does not trigger setters.

Schema-validate request bodies

Reject the dangerous keys before any merge, recursively and after decoding:

  • Allow-list the expected shape with a schema validator and drop everything else.
  • Reject __proto__, constructor, and prototype as keys at every level, as a defence-in-depth layer, not the only control.
  • Remember a block-list alone loses to constructor.prototype and nesting bypasses, which is why the schema should be allow-list first.

Object.freeze(Object.prototype): useful, not complete

Freezing the prototype at startup blocks the most-targeted write, but it is not a full fix:

Object.freeze(Object.prototype);   // blocks writes to Object.prototype
  • Other prototypes remain pollutable (Array.prototype, Function.prototype).
  • It can break legitimate code that extends prototypes (polyfills, some test tooling).
  • Treat it as defence in depth alongside the real fixes, never as the whole answer.

Keep dependencies current

Many prototype-pollution bugs live in third-party merge, query-string, and clone utilities, and many RCE gadgets live in template engines and older Node child_process. Keep Node and dependencies patched so known sources and gadgets are closed.

The layered picture

  • Primary: untrusted keys never reach an assignment on a pollutable object (safe merges, null-prototype maps, Map).
  • Supporting: allow-list schema validation, Object.freeze(Object.prototype), current dependencies.

With the primary control in place, there is no write for an attacker to make. The rest raises the bar if something slips.

For the attack side of each of these, work back through the cluster from the pillar, and keep the cheat sheet handy. Ready to prove the weak controls fall? Sign up free and try it.

← Back to Prototype Pollution: The Complete Practical Guide