Prototype pollution is a JavaScript-specific vulnerability that exploits the language's prototype-based inheritance model. By injecting properties into Object.prototype, an attacker can modify the behavior of every object in the application. Properties set on the prototype are inherited by all objects, meaning a single pollution can influence code execution paths throughout the entire application.
This is not a niche concern. Prototype pollution CVEs have been filed against lodash, jQuery, minimist, qs, express, and dozens of other widely used npm packages. The vulnerability is endemic to the JavaScript ecosystem because the language's core design -- prototypal inheritance and dynamic property access -- makes pollution natural and prevention unobvious.
How Prototypal Inheritance Works
In JavaScript, every object has a prototype. When you access a property on an object, JavaScript first checks the object itself, then walks up the prototype chain until it finds the property or reaches null.
const obj = {};
console.log(obj.toString); // Found on Object.prototype
Object.prototype sits at the top of the chain for most objects. Any property set on Object.prototype appears on every object that does not explicitly override it:
Object.prototype.polluted = true;
const obj = {};
console.log(obj.polluted); // true
This is the core of the attack. If an attacker can set a property on Object.prototype, that property is inherited by all objects in the application.
Exploitation Vectors
Recursive merge/extend functions. The most common vector. Functions that recursively merge objects are vulnerable when they process the __proto__ property:
function merge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object') {
target[key] = target[key] || {};
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
// Attacker-controlled input
const malicious = JSON.parse('{"__proto__": {"admin": true}}');
merge({}, malicious);
// Now every object has admin: true
const user = {};
console.log(user.admin); // true
Query string parsing. Libraries that parse nested query strings can be exploited:
?__proto__[admin]=true
?constructor[prototype][admin]=true
If the parser creates nested objects from the query string, it can set properties on Object.prototype through __proto__ or constructor.prototype.
JSON.parse with proto. JSON.parse('{"__proto__": {"x": 1}}') creates an object with a __proto__ property. If this object is then merged into another object using a vulnerable merge function, pollution occurs. Note that JSON.parse itself does not pollute -- the pollution happens when the parsed object is processed by vulnerable code.
Path-based property assignment. Functions that set nested properties based on dot-notation or bracket-notation paths:
function setProperty(obj, path, value) {
const parts = path.split('.');
let current = obj;
for (let i = 0; i < parts.length - 1; i++) {
current[parts[i]] = current[parts[i]] || {};
current = current[parts[i]];
}
current[parts[parts.length - 1]] = value;
}
// path = "__proto__.admin", value = true
setProperty({}, "__proto__.admin", true);
// Object.prototype.admin is now true
Real-World Impact
Prototype pollution alone modifies object properties. The impact depends on how the application uses those properties:
Authentication bypass. If the application checks user.admin or user.role and these properties are not explicitly set on the user object, they fall through to the prototype. Polluting Object.prototype.admin = true makes every user an admin.
Remote code execution. In Node.js, prototype pollution can lead to RCE through several gadget chains:
- Polluting
Object.prototype.shellorObject.prototype.NODE_OPTIONSbefore achild_process.spawn()call - Polluting template engine options (e.g.,
Object.prototype.allowProtoMethodsByDefault = truein Handlebars) - Polluting
Object.prototype.constructorto manipulate class instantiation
The Handlebars + prototype pollution to RCE chain is well documented:
- Pollute
Object.prototype.allowProtoMethodsByDefaultandObject.prototype.allowProtoPropertiesByDefaulttotrue - Craft a Handlebars template that accesses
constructor.constructorto get theFunctionconstructor - Execute arbitrary code through the
Functionconstructor
Denial of service. Polluting properties that are checked in loops or conditions can cause crashes, infinite loops, or unexpected behavior:
Object.prototype.length = 0; // Breaks for...in loops
Object.prototype.then = function() {}; // Makes all objects look like Promises
Property injection. Polluting properties used in SQL query construction, HTTP headers, or file paths can lead to injection attacks.
Prevention Techniques
Use Object.create(null) for lookup objects. Objects created with Object.create(null) have no prototype chain, so they are immune to prototype pollution:
const lookup = Object.create(null);
lookup["key"] = "value";
console.log(lookup.admin); // undefined, even if Object.prototype.admin is polluted
Use Map instead of plain objects. Map does not use prototypal inheritance for key-value storage:
const config = new Map();
config.set("key", "value");
config.get("admin"); // undefined, immune to pollution
Block proto in input. Sanitize all user input to remove __proto__, constructor, and prototype properties:
function sanitize(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
const clean = {};
for (const key of Object.keys(obj)) {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
clean[key] = sanitize(obj[key]);
}
return clean;
}
Use Object.freeze(Object.prototype). Freezing the prototype prevents any modifications. This is a nuclear option that may break libraries that intentionally modify prototypes:
Object.freeze(Object.prototype);
Use hasOwnProperty checks. When checking for properties that might be inherited, use hasOwnProperty or the in operator with explicit prototype chain exclusion:
if (user.hasOwnProperty('admin') && user.admin) {
// Only true if admin was explicitly set on this object
}
Use safe merge functions. When implementing or selecting merge/extend utilities, ensure they skip __proto__ and constructor.prototype:
function safeMerge(target, source) {
for (const key of Object.keys(source)) {
if (key === '__proto__' || key === 'constructor') continue;
if (typeof source[key] === 'object' && source[key] !== null) {
target[key] = target[key] || {};
safeMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
Supply Chain Considerations
Prototype pollution is a supply chain problem. Your application may not contain any vulnerable merge functions, but your dependencies might. A single vulnerable transitive dependency can pollute the prototype and affect your entire application.
Audit your dependencies:
- Check npm audit for known prototype pollution CVEs
- Review merge/extend functions in critical dependencies
- Monitor for new CVEs in lodash, qs, minimist, and other commonly affected packages
Runtime protection:
- Use
--frozen-intrinsicsflag in Node.js (experimental) to freeze built-in prototypes - Implement a Content Security Policy that mitigates client-side pollution to XSS chains
- Use RASP solutions that detect prototype pollution at runtime
Testing for Prototype Pollution
Manual testing:
- Submit
{"__proto__": {"testPollution": "true"}}in JSON request bodies - Submit
?__proto__[testPollution]=truein query strings - Check if a subsequent request to a different endpoint reflects the polluted property
Automated tools:
- ppfuzz -- prototype pollution fuzzer
- ppmap -- client-side prototype pollution scanner
- Burp Suite extensions for prototype pollution detection
How Safeguard.sh Helps
Safeguard.sh is essential for defending against prototype pollution because the vulnerability most commonly originates in third-party npm packages. Safeguard.sh continuously monitors your JavaScript dependency tree for packages with known prototype pollution CVEs. When a vulnerability is discovered in a library like lodash, qs, or minimist, Safeguard.sh alerts your team and identifies exactly which projects in your organization are affected. This supply chain visibility is critical because prototype pollution in a single transitive dependency can compromise every application that includes it.