JSON Toolkit

What is __proto__ injection

Bottom line: JSON.parse treats __proto__ as an ordinary key; the damage happens in a recursive merge that writes to Object.prototype, so every object inherits attacker properties.

Advanced~5 min readPublished 2026-10-30Updated 2026-10-30
How prototype pollution works and how to block it

Bottom line: block __proto__, constructor and prototype before any merge, merge untrusted input into Object.create(null), and add a prototype assertion to CI; patching libraries alone is not enough.

In one sentence: an untrusted key named __proto__ meets a recursive merge, and the merge writes to the prototype.

A reproduction

function merge(target, source) {
  for (const key in source) {
    if (typeof source[key] === 'object' && source[key] !== null) {
      target[key] = target[key] || {};
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

merge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));
console.log({}.isAdmin);   // true

No business object was touched. Object.prototype was, and every object now carries isAdmin.

Why JSON.parse is not at fault

const data = JSON.parse('{"__proto__": {"a": 1}}');
Object.getOwnPropertyNames(data);                  // [ '__proto__' ]
Object.getPrototypeOf(data) === Object.prototype;  // true, untouched

__proto__ arrives as a plain own property. The danger is entirely in the code that later merges, deep-copies, or sets by path — _.merge, some Object.assign patterns, and countless hand-written set(obj, path, value) helpers.

Impact

  • Authorisation bypass: if (user.isAdmin) becomes unconditionally true
  • Data corruption: unexpected fields serialise into stored records
  • Denial of service: large objects attached to the prototype grow memory permanently
  • Escalation: combined with template engines or command runners, this has led to remote code execution

Four defences

Filter the keys.

const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);

Blocking only __proto__ is insufficient: constructor.prototype reaches the same place.

Merge into a null-prototype object.

const safeTarget = Object.create(null);

With no prototype chain, __proto__ is just a string key.

Use Object.hasOwn instead of in. in consults the prototype chain.

Keep dependencies patched. lodash, deepmerge and json-patch have all shipped fixes for this class of bug. Watch npm audit.

Verify it stays fixed

Feed a payload containing __proto__ and constructor through your merge, then assert:

console.log({}.polluted);                 // undefined
console.log(Object.prototype.polluted);   // undefined

One assertion in CI, near-zero cost, catches regressions from later refactors.

FAQ

What keys should a merge filter out?

Block __proto__, constructor and prototype together. Filtering only __proto__ is insufficient, because constructor.prototype reaches the same prototype.

Why does Object.create(null) stop the write?

A null-prototype object has no prototype chain, so __proto__ stays an ordinary string key. It works as a data container, but it does not replace key filtering or dependency updates.

How do I test for prototype pollution in CI?

Send a payload containing __proto__ and constructor through your merge, then assert that {}.polluted and Object.prototype.polluted are undefined. One assertion costs almost nothing and catches regressions from later refactors.

Further reading