Prototype Pollution in JavaScript: How It Works and How to Stop It
Prototype pollution is a class of JavaScript vulnerability where an attacker can inject properties into the global Object.prototype, affecting every object in the application and potentially enabling property injection, denial of service, or even remote code execution. If you're building Node.js backends or complex front-end applications, understanding prototype pollution is non-negotiable.
How JavaScript Prototypes Work
To understand prototype pollution, you first need to understand JavaScript's prototype chain. Every JavaScript object has an internal link to a prototype object. When you access a property on an object, JavaScript walks up the prototype chain until it finds the property or reaches null.
const obj = {};
console.log(obj.toString); // inherited from Object.prototypeThis means Object.prototype is the root of every object in JavaScript. Any property added to Object.prototype becomes accessible on every plain object in the runtime.
What Is Prototype Pollution?
Prototype pollution occurs when an attacker controls user-supplied input that is used to set properties on objects using bracket notation or recursive merge/clone functions — and can craft that input to target __proto__, constructor, or constructor.prototype.
The classic attack vector:
const userInput = JSON.parse('{"__proto__": {"admin": true}}');
const merged = Object.assign({}, userInput);
const target = {};
console.log(target.admin); // true — Object.prototype was pollutedAfter the merge, Object.prototype.admin is true. Every newly created plain object now has an admin property, even objects that should be unprivileged.
Attack Vectors: __proto__, constructor, and Merge Functions
Via __proto__
The most direct path. If a recursive merge function processes __proto__ as a key:
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
if (!target[key]) target[key] = {};
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
merge({}, JSON.parse('{"__proto__":{"polluted":"yes"}}'));
console.log({}.polluted); // 'yes'Via constructor.prototype
A secondary path that bypasses __proto__ filters:
merge({}, JSON.parse('{"constructor":{"prototype":{"polluted":"yes"}}}'));
console.log({}.polluted); // 'yes'Via Deep Clone Libraries
Many popular libraries had prototype pollution vulnerabilities historically: lodash (CVE-2019-10744), jquery (CVE-2019-11358), hoek, merge, deep-extend. A malicious payload passed to a deep clone or deep merge function in an older version of these libraries could pollute the prototype.
The Path to Remote Code Execution
Prototype pollution becomes truly critical when it can be escalated to RCE. This happens when application code uses object properties to make security decisions or build system commands — and those properties can be set via prototype pollution.
A famous example from Node.js applications using child_process:
// Attacker pollutes: {"__proto__": {"shell": true, "env": {"NODE_OPTIONS": "--require /tmp/malicious.js"}}}
const options = {}; // inherits shell: true and env from prototype
const result = child_process.execFile('/usr/bin/node', ['script.js'], options);
// NODE_OPTIONS causes Node to require the attacker's fileCVE-2022-21824 (Node.js console.table) is a real-world example where prototype pollution in the standard library led to exploitable conditions.
Real-World Impact and CVEs
- CVE-2019-10744 — Lodash
_.defaultsDeep,_.merge,_.mergeWith: prototype pollution via user-controlled keys. Affected millions of projects. - CVE-2019-11358 — jQuery
$.extenddeep merge: prototype pollution leading to XSS in applications using the result in DOM operations. - CVE-2020-8203 — Lodash
_.zipObjectDeep: another prototype pollution via controlled key names. - CVE-2022-21824 — Node.js: prototype pollution in
console.tablecould be exploited via crafted object iteration.
These are not theoretical bugs. They affected production systems at scale.
Detection: How to Find Prototype Pollution
Manual Testing
Inject the following payloads into any endpoint that accepts JSON or object-like input:
{"__proto__": {"testprop": "polluted"}}
{"constructor": {"prototype": {"testprop": "polluted"}}}Then check whether {}.testprop returns 'polluted' in the application context. Browser DevTools or server-side logging can reveal this.
Automated Detection
Tools like @nicolo-ribaudo/semver-store audits, snyk, and DAST scanners can identify vulnerable merge patterns. Static analysis tools like ESLint with security plugins can flag unsafe key assignments.
Prevention: Five Solid Defenses
1. Freeze Object.prototype
The nuclear option — prevents any modification to the base prototype:
Object.freeze(Object.prototype);This is highly effective but can break some third-party libraries. Test thoroughly before applying in production.
2. Use Object.create(null)
Create objects with no prototype at all for data storage and lookup maps:
const safeMap = Object.create(null);
// safeMap has no __proto__, constructor, or any inherited properties
safeMap['userInput'] = value; // safeThis is the recommended pattern for any object used as a key-value store with untrusted keys.
3. Sanitize Keys Before Merge
Explicitly block dangerous keys in merge/clone functions:
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
function safeMerge(target, source) {
for (let key in source) {
if (DANGEROUS_KEYS.has(key)) continue;
if (typeof source[key] === 'object') {
if (!target[key]) target[key] = {};
safeMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}4. Use hasOwnProperty Checks
When iterating object properties, always use hasOwnProperty or Object.hasOwn to avoid traversing the prototype chain with untrusted data:
for (const key in source) {
if (Object.hasOwn(source, key)) {
// process key
}
}5. Keep Dependencies Updated
The majority of real-world prototype pollution attacks target library vulnerabilities. Run npm audit regularly, use snyk or Dependabot, and upgrade lodash, jQuery, and merge utilities to patched versions immediately.
Node.js-Specific Mitigations
Node.js 22+ includes additional hardening options. Running with --frozen-intrinsics freezes all built-in objects, including Object.prototype. This is the most comprehensive defense for production Node.js services.
Additionally, consider using a schema validation layer (Joi, Zod, Ajv) on all incoming JSON. If your schema doesn't allow __proto__ as a key, the payload never reaches your merge functions.
ZeriFlow and Dependency Risk
Prototype pollution is fundamentally a code-level vulnerability, but your application's overall security posture — including the headers, configurations, and dependency hygiene that automated scanners assess — directly affects how exploitable these flaws are.
FAQ
Q: Is prototype pollution only a server-side (Node.js) issue?
A: No. Prototype pollution affects both client-side JavaScript and Node.js. In browsers, polluted prototypes can lead to XSS or DOM-based attacks if the polluted properties influence DOM manipulation code. In Node.js, the impact can escalate to RCE.
Q: Does using TypeScript protect against prototype pollution?
A: Partially. TypeScript's type system can prevent some accidental property assignments at compile time, but TypeScript doesn't exist at runtime. Malicious JSON input bypasses TypeScript entirely. You still need runtime defenses like Object.freeze or key sanitization.
Q: Can JSON.parse itself cause prototype pollution?
A: Native JSON.parse in modern browsers and Node.js does not set __proto__ on the parsed object — it creates a literal key called __proto__. However, when that parsed object is passed to a vulnerable merge function, the merge function walks the key chain and sets it on the prototype. The bug is in the merge, not the parse.
Q: What's the difference between prototype pollution and prototype poisoning?
A: These terms are often used interchangeably in security contexts. 'Prototype pollution' is the more widely used and standardized term in the security community (used by OWASP and CVE descriptions). Both refer to the same class of vulnerability.
Q: Which npm packages are most commonly affected?
A: Historically: lodash (multiple CVEs), jquery, hoek, merge, deep-extend, node-forge, immer, and minimist. Always check npm audit and review your dependency tree for these packages, especially older versions.
Conclusion
Prototype pollution is a subtle but high-impact vulnerability class that exploits one of JavaScript's most fundamental design decisions. The key defenses are simple in principle: freeze the prototype, use Object.create(null) for maps, sanitize merge keys, and keep your dependencies updated.
As JavaScript applications grow more complex and rely on more third-party libraries, the attack surface for prototype pollution grows with it. Building these defenses into your standard coding patterns — not as an afterthought — is the right approach.