Prototype Pollution
Prototype pollution is a JavaScript-specific vulnerability class where an attacker injects properties into Object.prototype, affecting every object in the application. It is less well-known than XSS or CSRF, but has led to serious real-world exploits β including remote code execution in Node.js.
How JavaScript Prototypes Workβ
Every JavaScript object has an internal link to another object called its prototype. When you access a property that doesn't exist on the object itself, the engine walks up the prototype chain until it finds the property or reaches null. For ordinary objects, the chain ends at Object.prototype β the root prototype shared by nearly all objects.
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null
This section is a refresher, not a tutorial. For a full explanation, see MDN: Inheritance and the prototype chain.
What Is Prototype Pollution?β
If an attacker can set a property on Object.prototype, that property becomes the default value for every object in the application β unless the object already has its own property with the same name.
// Attacker-controlled input
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
// Vulnerable recursive merge
merge({}, malicious);
// Now every object "inherits" isAdmin
const user = {};
console.log(user.isAdmin); // true
The dangerous operation is the merge function writing to target["__proto__"]["isAdmin"]. Because obj.__proto__ is a reference to Object.prototype, this is equivalent to Object.prototype.isAdmin = true β and from that point on, every newly created object (and every existing object without its own isAdmin property) will report isAdmin as true.
How Attacks Happen in Practiceβ
Vulnerable Recursive Mergeβ
A naΓ―ve deep merge function that copies all properties β including __proto__ β is the most common vulnerable pattern:
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]); // Recursing into __proto__
} else {
target[key] = source[key]; // Writing to Object.prototype
}
}
return target;
}
When key is "__proto__", the recursive call operates on Object.prototype instead of a regular nested object. Any properties set during that recursion are injected into the prototype chain globally.
Common Entry Pointsβ
Prototype pollution doesn't require a custom merge function. It can enter through any code path that assigns user-controlled keys to objects:
- Query string parsers β A URL like
?__proto__[isAdmin]=truecan pollute the prototype if the parser builds a nested object without filtering keys. - JSON body parsing β API endpoints that accept arbitrary JSON and merge it into an existing object.
- Deep merge / deep clone utilities β Any library function that recursively copies properties without guarding against prototype keys.
- Configuration merging β Combining default config with user overrides using an unsafe merge.
Object.assign does not recursively walk nested objects, so it does not trigger the same recursive __proto__ merge pattern shown above. However, copying untrusted keys with Object.assign can still poison the target object's prototype if __proto__, constructor, or prototype are not filtered.
Real-World Vulnerabilitiesβ
| Library | CVE | Affected versions | Fix |
|---|---|---|---|
lodash merge, defaultsDeep | CVE-2018-16487, CVE-2019-10744 | < 4.17.12 | Added guards for __proto__ keys |
jQuery $.extend (deep mode) | CVE-2019-11358 | < 3.4.0 | Added __proto__ check |
| minimist | CVE-2020-7598, CVE-2021-44906 | < 1.2.6 | Added key filtering |
| qs | CVE-2022-24999 | < 6.10.3 | Restricted prototype keys |
These are among the most widely-installed packages in the npm ecosystem. The vulnerabilities affected millions of downstream projects.
Impactβ
Remote Code Execution (Node.js)β
In Node.js, polluting properties like shell, env, or NODE_OPTIONS on Object.prototype can lead to remote code execution when child processes are spawned. Functions like child_process.exec() read options from the object passed to them β and if that object doesn't explicitly set shell, it falls through to the polluted prototype value.
Authentication Bypassβ
If authorization logic checks properties with truthiness (if (user.isAdmin)), polluting isAdmin, role, or verified on Object.prototype grants those properties to every object β including user objects that should not have them.
Denial of Serviceβ
Polluting toString or valueOf with non-function values causes crashes whenever code implicitly calls these methods β which happens in string concatenation, template literals, comparison operations, and many library internals.
Cross-Site Scriptingβ
On the client side, polluted prototype properties can be consumed by template engines, DOM manipulation libraries, or framework internals. If a rendering library reads a property like innerHTML or src from the prototype chain, an attacker can inject content into the page.
Preventionβ
Use Object.create(null) or Mapβ
Object.create(null) creates an object with no prototype β it doesn't inherit from Object.prototype, so prototype pollution has no effect on it:
// Safe: no prototype chain
const safeDict = Object.create(null);
safeDict["__proto__"] = { polluted: true };
console.log({}.polluted); // undefined β Object.prototype was not modified
For arbitrary key-value pairs from user input, Map is the correct data structure:
// Safe: Map doesn't use Object.prototype for storage
const userInput = new Map();
userInput.set("__proto__", { polluted: true });
console.log({}.polluted); // undefined
For deep cloning, use the built-in structuredClone() (available in all modern browsers and Node.js 17+). It does not copy prototype properties and is not vulnerable to prototype pollution.
Guard Merge Functionsβ
If you write a recursive merge or deep clone function, skip the dangerous keys:
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
function safeMerge(target, source) {
for (const key of Object.keys(source)) {
if (UNSAFE_KEYS.has(key)) continue;
if (typeof source[key] === 'object' && source[key] !== null) {
if (!target[key]) target[key] = {};
safeMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
Alternatively, use a library that already handles this. lodash versions β₯ 4.17.12 guard against prototype pollution in merge and defaultsDeep.
Validate and Sanitize Inputβ
Strip or reject __proto__, constructor, and prototype keys from user-controlled JSON before processing.
Freeze the Prototype (Nuclear Option)β
Object.freeze(Object.prototype) prevents any modification to the root prototype:
Object.freeze(Object.prototype);
This can break third-party libraries that add or modify properties on Object.prototype β some polyfills and older libraries do this intentionally. Test thoroughly before deploying this in a real application.
ESLintβ
Two built-in ESLint rules help catch prototype-related issues at the code level:
no-protoβ disallows use of the__proto__property.no-extend-nativeβ disallows extending native prototypes (e.g.,Object.prototype.myMethod = ...).
Both are available in ESLint core and can be enabled without any additional plugins.
Further Readingβ
- Prototype Pollution β PortSwigger Research
- MDN: Inheritance and the prototype chain
- Snyk: Prototype Pollution
- Web Security Essentials β overview of web security fundamentals
- npm Supply Chain Attacks β supply chain security for the npm ecosystem