What Is Prototype Pollution in JavaScript?

Prototype pollution is a JavaScript vulnerability that lets an attacker inject properties into the base template (prototype) that all objects in an application inherit from. Because nearly every object in JavaScript shares a common root prototype, poisoning that single root can change the behavior of every object in the entire program. The consequences range from crashing an application to full remote code execution on a server.

How JavaScript Prototypes Work

JavaScript is a prototype-based language. When you create an ordinary object, it doesn’t exist in isolation. It’s linked to a prototype object, and if you try to access a property that doesn’t exist on the object itself, JavaScript walks up the chain and checks the prototype. For most objects, that chain eventually leads to Object.prototype, a shared template sitting at the top.

This inheritance system is what makes the vulnerability possible. If an attacker can add a property to Object.prototype, that property becomes visible on every ordinary object in the application that hasn’t explicitly defined its own version of that property. It’s like editing a master blueprint that thousands of buildings were constructed from: every building changes at once.

The Attack Mechanism

The core of the attack relies on two access paths to an object’s prototype. The first is the __proto__ property, which directly exposes the prototype of any object. The second is the constructor.prototype path, which reaches the same destination through a different route. Even if a developer disables __proto__, the constructor.prototype path can still be used.

The vulnerable code pattern looks like this:

obj[key1][key2] = value;

If key1 is "__proto__" and key2 is any property name, that statement doesn’t add a nested property to the object. Instead, the JavaScript engine treats __proto__ as a getter for the prototype, and the assignment lands on Object.prototype itself. Every object that inherits from that prototype now has the new property.

The three-key variant works the same way through the constructor path:

obj[key1][key2][key3] = value;

Here, key1 is "constructor", key2 is "prototype", and key3 is the property name. The result is identical: a property injected into the shared root prototype.

Where Vulnerable Code Appears

Prototype pollution typically shows up in functions that recursively merge or clone objects using user-controllable input. Think of utility functions that deep-merge configuration objects, set nested properties by path, or combine default settings with user-provided overrides. These are extremely common in JavaScript applications.

When a merge function encounters a key like __proto__ in the incoming data, it doesn’t treat it as a special case. It walks into the prototype as if it were just another nested object and starts assigning properties there. The function thinks it’s doing a normal deep merge. In reality, it’s writing to the shared prototype.

JSON payloads are a common delivery method. While JSON.parse() itself treats __proto__ as a regular string key (creating a normal property on the parsed object), problems start when that parsed object is later passed into a merge or clone function that does interpret __proto__ as a prototype accessor. The combination of parsing and merging creates the vulnerability.

Real-World Impact

The lodash library, one of the most widely used JavaScript utility packages, had a critical prototype pollution vulnerability (CVE-2019-10744) in all versions before 4.17.12. Its defaultsDeep function could be tricked into modifying Object.prototype using a constructor payload. Given that lodash is a dependency in millions of projects, the blast radius was enormous.

Research presented at USENIX Security 2023 demonstrated that prototype pollution combined with existing code patterns (“gadgets”) in Node.js can escalate all the way to remote code execution. The researchers found that legitimate code throughout the Node.js platform reads properties from object prototypes at runtime. If an attacker has already polluted those prototypes with carefully chosen values, that legitimate code can be hijacked to execute arbitrary commands on the server. The paper called the evidence “alarming,” noting that the path from pollution to full server compromise was shorter than previously assumed.

The three main consequences of a successful attack are:

  • Denial of service: injecting properties that cause application logic to crash or loop
  • Privilege escalation: adding an isAdmin property to the root prototype so that every user object suddenly appears to have admin access
  • Remote code execution: triggering existing code paths that read from the polluted prototype and execute system commands

Client-Side vs. Server-Side Risks

On the server side (Node.js), prototype pollution is particularly dangerous because it can lead to remote code execution. Server applications often run merge operations on data received from HTTP requests, and the polluted prototype persists for the lifetime of the process, affecting every subsequent request.

In the browser, the attack surface is different but still significant. Client-side prototype pollution typically arrives through URL parameters, JSON responses, or postMessage data that feeds into a vulnerable merge function. The polluted prototype can then influence how the page renders, alter security checks in client-side logic, or be chained with cross-site scripting (XSS) vectors. Because each browser tab runs its own JavaScript context, the pollution is scoped to that tab, but that’s often enough to compromise the user’s session.

How to Prevent It

The most direct defense is sanitizing keys before any dynamic property assignment. If your code is about to set a property using a user-supplied key, reject __proto__, constructor, and prototype as key names. This single check blocks both access paths to the prototype.

For objects that serve as key-value stores (configuration objects, caches, lookup tables), create them with Object.create(null) instead of using a plain object literal. Objects created this way have no prototype at all, so there’s nothing to pollute. Alternatively, use a Map instead of a plain object. Maps don’t participate in prototype inheritance, so setting a key called "__proto__" just creates a normal entry.

You can also freeze the root prototype with Object.freeze(Object.prototype). This prevents any new properties from being added to it. The trade-off is that some libraries may rely on modifying prototypes at runtime, so freezing can break things in unpredictable ways. Test thoroughly before deploying this in production.

Node.js provides a command-line flag, --disable-proto, that disables the __proto__ property entirely. This closes the most common attack vector at the platform level, though it doesn’t protect against the constructor.prototype path. It’s a useful layer of defense, not a complete solution on its own.

Finally, keep your dependencies updated. The lodash vulnerability was patched in version 4.17.12, but countless projects still pin older versions. Automated dependency scanners (npm audit, Snyk, or similar tools) can flag known prototype pollution vulnerabilities in your dependency tree before they reach production.