Skip to main content
Back to blog
April 28, 2026|8 min read|Antoine Duno

Prototype Pollution in JavaScript: How It Works and How to Stop It

Prototype pollution is a JavaScript vulnerability that lets attackers inject properties into the base Object prototype, potentially leading to remote code execution. Here's the complete guide.

ZeriFlow Team

1,417 words

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.

Scan your web application for security misconfigurations with ZeriFlow — 80+ checks, completely free.


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.

javascript
const obj = {};
console.log(obj.toString); // inherited from Object.prototype

This 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:

javascript
const userInput = JSON.parse('{"__proto__": {"admin": true}}');
const merged = Object.assign({}, userInput);

const target = {};
console.log(target.admin); // true — Object.prototype was polluted

After 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:

javascript
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:

javascript
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:

javascript
// 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 file

CVE-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 $.extend deep 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.table could 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:

json
{"__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:

javascript
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:

javascript
const safeMap = Object.create(null);
// safeMap has no __proto__, constructor, or any inherited properties
safeMap['userInput'] = value; // safe

This 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:

javascript
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:

javascript
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.

Run a free ZeriFlow scan to identify the security header gaps and configuration issues that compound vulnerabilities like prototype pollution.


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.

Scan your application with ZeriFlow today and get a clear picture of your web security posture in minutes.

Ready to check your site?

Run a free security scan in 30 seconds.

Related articles

Keep reading