Explore the dangers of JavaScript prototype pollution and how it can compromise security in web applications. Understand how to prevent this vulnerability.
You merge a chunk of user-supplied JSON into a plain object. Looks harmless. A moment later every object in your app answers true to an isAdmin check you never wrote, and a login gate you trust starts waving strangers through. That’s prototype pollution, and it grows straight out of the feature that makes JavaScript objects tick: inheritance through prototypes.
JavaScript runs in the browser and on the server, and that reach is exactly why this bug stings. Poison one shared prototype and you’ve touched every object that inherits from it. In this guide we’ll walk through how the attack works, what it actually costs you, the real libraries it has burned, and the short list of defenses that stop it. We’ll go from a beginner example up to the pattern that has caused real CVEs, so you can spot it in your own code.
Table of Contents
- What is JavaScript Prototype Pollution?
- How Prototype Pollution Works
- The Impact of Prototype Pollution
- Real-World Examples of Prototype Pollution
- Defending Against Prototype Pollution
- Conclusion
What is JavaScript Prototype Pollution?
Prototype pollution happens when an attacker can change an object’s prototype at runtime, usually Object.prototype itself. Because nearly every object inherits from Object.prototype, a change there ripples out to all of them. Add one property to the prototype and, from that point on, every object claims to have it.
Here’s the mechanism. Every object in JavaScript has an internal link, [[Prototype]], pointing at its prototype. When you read a property the object doesn’t own, the engine walks up that chain looking for it. That’s why an empty {} still has toString(): it borrows it from Object.prototype. The attacker’s goal is to write to that shared parent, so every child inherits whatever they plant.
Why is Prototype Pollution Dangerous?
When an attacker can write to a shared prototype, they don’t compromise one object, they compromise all of them at once. A property planted on Object.prototype shows up on config objects, request objects, permission objects, everything. On its own that can flip a security check. Chained with the right “gadget” already in your code or a dependency, it can escalate to something much worse, which is what makes it more than a curiosity.
How Prototype Pollution Works
At its core, prototype pollution happens when user-controlled input gets written into the prototype chain. It shows up wherever an app copies or merges data into objects without checking the keys first. Start with the simplest version so the mechanic is clear.
Basic Example of Prototype Pollution
/**
* Demonstrates how prototype pollution works by adding a property to the prototype chain.
* @example
* let obj = {};
* obj.__proto__.polluted = 'Yes, I am polluted!';
* console.log({}.polluted); // Outputs: 'Yes, I am polluted!'
*/
let obj = {};
obj.__proto__.polluted = 'Yes, I am polluted!';
console.log({}.polluted); // Outputs: 'Yes, I am polluted!'
obj.__proto__ is Object.prototype. Writing to it sets a property on the shared parent, so a brand-new {} that never touched your code already inherits polluted. Nobody writes this line on purpose. The danger is when attacker-controlled keys reach a spot that does the equivalent for you.
Explaining the Prototype Chain
So the chain is the whole story. A fresh {} inherits methods like toString() from Object.prototype. Change something on that prototype and you haven’t changed one object, you’ve changed every object that shares it. That shared-parent behavior is the leverage the attack depends on.
Example of Prototype Pollution in User Input
A frequent starting point is parsing JSON and copying it straight onto an object. It’s worth being precise here, because this exact snippet is often shown as global pollution and it isn’t quite. Watch what really happens.
/**
* Demonstrates prototype pollution through user input manipulation.
* @example
* let userInput = JSON.parse('{"__proto__": {"admin": true}}');
* Object.assign(obj, userInput);
* console.log({}.admin); // Outputs: undefined
*/
let userInput = JSON.parse('{"__proto__": {"admin": true}}');
let obj = {};
Object.assign(obj, userInput);
console.log(obj.admin); // Outputs: true, obj's own prototype was reassigned
console.log({}.admin); // Outputs: undefined, Object.prototype is untouched
Two things worth getting right. JSON.parse gives userInput a real own key named __proto__ (the parser bypasses the setter). But Object.assign copies with normal assignment, which triggers the __proto__ setter and reassigns obj‘s own prototype to {admin: true}. So obj inherits admin, but Object.prototype and every other object are left alone. It’s still a bug, just a local one.
Global pollution, the kind that touches every object, needs code that walks into the value under __proto__ and writes onto it. That’s exactly what a naive recursive merge does:
/**
* A vulnerable recursive merge. Recursing through the '__proto__' key writes
* onto Object.prototype instead of the target, polluting every object.
* @example
* merge({}, JSON.parse('{"__proto__": {"admin": true}}'));
* console.log({}.admin); // Outputs: true
*/
function merge( target, source ) {
for ( const key in source ) {
if ( source[ key ] && typeof source[ key ] === 'object' ) {
if ( ! target[ key ] ) {
target[ key ] = {};
}
merge( target[ key ], source[ key ] );
} else {
target[ key ] = source[ key ];
}
}
return target;
}
merge( {}, JSON.parse( '{"__proto__": {"admin": true}}' ) );
console.log( {}.admin ); // Outputs: true, every object is now polluted
Here target[key] for key === '__proto__' reads Object.prototype, and the recursive call assigns admin onto it. Now any {}.admin is true. This is the pattern behind the real-world library bugs below, so if you write merge, clone, or “set by path” helpers, this is the shape to fear.
The Impact of Prototype Pollution
Once a prototype is polluted, the fallout ranges from a wobbly app to a full compromise, depending on what else is lying around to abuse. The common outcomes:
- Denial of Service (DoS): overwriting inherited properties like
toStringorvalueOfcan crash code that assumed the originals, taking the app down. - Remote Code Execution: pollution alone rarely runs code. Chained with a “gadget,” a spot that later reads an inherited property into something dangerous (spawn options, a template compiler, a serializer), it has led to RCE in Node.js apps.
- Privilege Escalation: where object properties gate permissions (think
isAdmin), a planted property can make every object look authorized and slip past the check.
Example: Manipulating Application Logic
Say an app decides access by reading user.isAdmin. Plant isAdmin: true on the prototype and every user object inherits it, so the gate opens for accounts that should never pass.
/**
* Example of how an attacker can manipulate application logic to gain unauthorized access.
* @example
* user.__proto__.isAdmin = true;
*/
let user = { name: 'regularUser' };
// Security check
if (user.isAdmin) {
console.log('Access granted.');
} else {
console.log('Access denied.');
}
// Attacker manipulates the prototype
user.__proto__.isAdmin = true;
console.log(user.isAdmin); // Outputs: true
The isAdmin line stands in for whatever unvalidated merge or path-set an attacker can actually reach in your app. The check never had a chance, because the value it trusted came from a prototype anyone could write.
Real-World Examples of Prototype Pollution
This isn’t theoretical. It has landed in libraries millions of projects depend on, which is what makes a single bad merge so far-reaching.
1. Lodash Library Prototype Pollution
Lodash, the widely used utility library, has shipped multiple prototype pollution fixes. defaultsDeep was vulnerable (CVE-2019-10744), and merge, mergeWith, and set were later patched too (CVE-2020-8203). All of them recurse through nested keys, so a crafted input carrying __proto__ could reach Object.prototype, the exact pattern shown above. Recent Lodash versions reject those keys; older ones are worth auditing out.
2. jQuery’s Vulnerability
jQuery was affected through $.extend(true, ...), its deep-copy helper (CVE-2019-11358, fixed in 3.4.0). If untrusted data reached a deep extend, an attacker could ride a __proto__ key into the prototype. Same root cause, different door: a recursive merge that trusts its keys.
Defending Against Prototype Pollution
The fixes aren’t exotic. They come down to distrusting keys and shrinking what a polluted prototype can reach. A few that pull real weight:
1. Avoid Using __proto__ Directly
Don’t build objects by writing to __proto__. When you need a plain map with no inherited baggage, reach for Object.create(null) or a real Map. Both sidestep Object.prototype entirely, so there’s no shared parent to poison and no __proto__ key to abuse.
2. Sanitize and Validate User Input
Before untrusted data lands in an object, reject the dangerous keys outright: __proto__, constructor, and prototype. Better still, validate the whole payload against a schema (with a tool like Ajv, Zod, or Joi) so only the fields you expect get through. Never feed raw user data into a recursive merge() or “set by path” helper without that gate in front of it.
3. Freeze What Shouldn’t Change
Object.freeze() blocks writes to an object. Freeze the shared prototype itself, Object.freeze(Object.prototype), early in startup and the classic global-pollution write simply fails (throws in strict mode, silently no-ops otherwise). Test it, since some libraries assume a writable prototype. Freezing your own critical config objects is a smaller, safer version of the same idea.
/**
* Use Object.freeze() to prevent critical objects from being modified.
* @example
* const secureObject = Object.freeze({ key: 'value' });
* secureObject.key = 'newValue'; // No effect
*/
const secureObject = Object.freeze({ key: 'value' });
// Attempt to modify the object
secureObject.key = 'newValue'; // This will not change the value4. Regularly Update Dependencies
Most prototype pollution bugs live in dependencies, not your own code (Lodash and jQuery being the obvious examples). Keep them current and patch promptly. A polluted prototype planted through a library is just as damaging as one you write yourself.
5. Use Security Tools
Let tooling catch what review misses. Snyk and npm audit flag known-vulnerable versions in your dependency tree, including the prototype pollution CVEs, and point you at the fixed release.
Conclusion
Prototype pollution is dangerous for one reason: it doesn’t attack an object, it attacks the parent every object shares. That’s how a single crafted key turns into flipped permissions, crashes, or a chain to code execution. The good news is the root cause is narrow, recursive writes that trust attacker-supplied keys, and so are the fixes.
Reject __proto__, constructor, and prototype before untrusted data hits an object. Reach for Map or Object.create(null) for user-keyed data, validate against a schema, freeze what shouldn’t change, and keep your dependencies current. Do that and this whole class of bug mostly stops being your problem.


