Articles

What Is Prototype Pollution? JavaScript's Sneaky Bug

Prototype pollution is a JavaScript vulnerability where attacker input modifies Object.prototype, affecting every object. How it works and how to stop it.

Chisato Chisato · · 5 min read
Source code in a dark-themed code editor

Prototype pollution is a JavaScript vulnerability in which attacker-controlled input adds or changes properties on a shared prototype, usually Object.prototype. Because almost every object in a program inherits from that prototype, a single polluted property suddenly appears on objects the attacker never touched. Depending on how the application uses those properties, the result ranges from crashes and logic bugs to authorization bypasses and, in server-side code, remote code execution.

Why prototypes make this possible

JavaScript objects do not copy behavior from classes. Instead, each object holds a hidden link to another object, its prototype, and property lookups walk up that chain until they find a match. If you need a refresher, see how prototypal inheritance works.

Plain objects created with {} link to Object.prototype. That means:

const a = {};
const b = {};

Object.prototype.isAdmin = true;

console.log(a.isAdmin); // true
console.log(b.isAdmin); // true

Neither a nor b has its own isAdmin property, but both find one on the shared prototype. Prototype pollution is any path that lets untrusted data reach that shared object and write to it.

How the attack actually happens

Applications rarely assign to Object.prototype on purpose. The danger comes from generic code that walks object keys supplied by the user. Three patterns show up again and again:

  • Recursive merge or deep extend. A helper merges a user-supplied JSON body into a configuration or defaults object.
  • Path-based setters. A function like set(obj, "a.b.c", value) builds nested objects from a dotted string.
  • Query-string and form parsers. Libraries that turn ?user[name]=x into nested objects.

The magic key is __proto__. In most engines, obj.__proto__ is an accessor that returns the object’s prototype. So a naive merge that does target[key] = source[key] recursively will, when it meets the key __proto__, step into Object.prototype and start writing there.

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

const payload = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, payload);

console.log({}.isAdmin); // true

JSON.parse creates __proto__ as an ordinary own property, so it survives parsing. The merge function then reads target["__proto__"], which resolves to Object.prototype, and writes isAdmin onto it. The keys constructor and prototype offer a second route: obj.constructor.prototype also points at Object.prototype for plain objects.

What an attacker can do with it

Polluting a prototype is only half the attack. The impact depends on a “gadget”: code elsewhere that reads a property it assumes is absent and behaves differently when it is suddenly present.

  • Authorization bypass. A check like if (user.isAdmin) passes for any user object that lacks its own isAdmin field.
  • Denial of service. Overwriting a commonly used method such as toString, or a property a framework iterates over, can crash every request in a Node.js process. Because the prototype is shared across the whole process, one malicious request can affect all users until a restart.
  • Client-side XSS. In the browser, polluted properties can flow into script-loading or HTML-rendering options in a library, turning into cross-site scripting.
  • Server-side code execution. In Node.js, polluted options that reach templating engines or functions that spawn child processes have been chained into remote code execution.

Defenses that work

There is no single switch that eliminates prototype pollution, but layered defenses make it very hard to exploit.

Reject dangerous keys. Any code that walks user-supplied keys should skip __proto__, constructor, and prototype. This is the most direct fix for custom merge and set helpers.

const BLOCKED = new Set(["__proto__", "constructor", "prototype"]);
for (const key of Object.keys(source)) {
  if (BLOCKED.has(key)) continue;
  // ...
}

Use objects without a prototype. Object.create(null) produces an object with no prototype at all, so __proto__ is just an ordinary key and there is nothing to pollute through it. This is a good choice for dictionaries built from user input.

Use Map for key-value data. A Map stores keys as data, not as properties, so arbitrary strings cannot reach a prototype.

Check own properties. When reading optional flags, use Object.hasOwn(obj, "isAdmin") instead of a truthiness check. That neutralizes many gadgets even if pollution happens somewhere.

Freeze the prototype. Calling Object.freeze(Object.prototype) early in startup makes writes to it fail silently (or throw in strict mode). Node.js also offers a --disable-proto flag that removes or neuters the __proto__ accessor. Test these carefully, since some older libraries modify built-in prototypes on purpose.

Validate input against a schema. Validating request bodies against an explicit schema that lists allowed fields stops unexpected keys at the edge. This is the same principle covered in input validation versus sanitization: define what valid input looks like and reject everything else.

Prefer safe cloning. For copying data, the built-in structuredClone or a spread into a fresh object avoids hand-rolled recursive merges entirely.

Dependencies are part of your attack surface

Many real prototype pollution bugs have lived in widely used utility libraries for deep merging, object path handling, and query-string parsing, rather than in application code. Keeping dependencies current and running a dependency audit in CI matters as much as fixing your own helpers. This is one specific case of the broader discipline of software supply chain security.

Prototype pollution compared with similar bugs

Prototype pollutionMass assignmentObject injection (other languages)
Root causeUser keys reach a shared prototypeUser keys set fields on a modelUntrusted data deserialized into objects
Blast radiusEvery object inheriting the prototypeThe single record being savedDepends on available classes
Typical fixBlock special keys, null-prototype objectsAllow-list writable fieldsAvoid native deserialization of untrusted data

The takeaway

Prototype pollution happens when untrusted keys like __proto__ or constructor flow through generic merge, set, or parse code and write onto Object.prototype, silently adding properties to every object in the program. Block those keys, use Object.create(null) or Map for user-keyed data, check own properties for security-relevant flags, validate input against a schema, and keep your utility dependencies patched.

Chisato Chisato · · 4 min read

Trusted Types API: Stopping DOM XSS at the Sink

The Trusted Types API blocks DOM-based XSS by forcing risky DOM sinks like innerHTML to accept only vetted objects instead of raw strings.

#Security #Web Development #JavaScript
Chisato Chisato · · 3 min read

What Is XSS? Cross-Site Scripting Explained

Cross-site scripting (XSS) injects malicious scripts into pages other users view. How stored, reflected, and DOM-based XSS work, and how to prevent them.

#Security #Web Development #JavaScript
Chisato Chisato · · 4 min read

What Is a Content Security Policy (CSP)?

A Content Security Policy is an HTTP header that restricts what scripts and resources a page can load, blocking most XSS attacks by default.

#Security #Web Development #JavaScript