What is Prototype Pollution? Ways to Exploit, Examples and Impact
Learn what prototype pollution is, how to exploit it with real-world examples, and how to prevent this critical JavaScript vulnerability in your apps.
Prototype pollution is a sophisticated vulnerability unique to JavaScript and other prototype-based languages that allows attackers to manipulate the behavior of an application by injecting properties into existing object prototypes. By modifying the base Object.prototype, an attacker can trigger side effects across the entire application, leading to severe consequences such as Cross-Site Scripting (XSS), privilege escalation, and even Remote Code Execution (RCE). In this guide, we will explore the mechanics of the prototype chain, the common patterns that lead to this vulnerability, and how you can defend your infrastructure against it.
Understanding the Foundation: JavaScript Prototypes
To understand how prototype pollution works, we must first understand how JavaScript handles inheritance. Unlike class-based languages like Java or C++, JavaScript uses prototypes. Every object in JavaScript has an internal property called [[Prototype]], which points to another object. This chain continues until it reaches an object with null as its prototype—usually Object.prototype.
When you attempt to access a property on an object, JavaScript first looks at the object itself. If the property is not found, it looks at the object's prototype. If it is still not found, it continues up the "prototype chain" until it either finds the property or reaches the end of the chain.
Consider this example:
const user = { name: "Alice" };
console.log(user.toString()); // [object Object]
The user object does not have a toString method defined directly. However, because user inherits from Object.prototype, it can access the toString method defined there.
In modern JavaScript, you can access an object's prototype via the __proto__ property (though it is deprecated in favor of Object.getPrototypeOf()). If an attacker can control the keys of an object being merged or assigned, they can use __proto__ to reach the base Object.prototype and add properties that will then be visible to every object in the runtime.
What is Prototype Pollution?
Prototype pollution occurs when an application improperly merges an untrusted JSON object into an existing object without validating the keys. If the input contains the key __proto__, the merge operation might unintentionally overwrite properties on the global Object.prototype.
Once Object.prototype is "polluted," every object created in the application after that point (and most existing ones) will inherit the polluted property. This is particularly dangerous because developers often assume that newly created objects are empty or contain only the properties they explicitly defined.
// A simple pollution example
let myObj = {};
let untrustedInput = JSON.parse('{"__proto__": {"isAdmin": true}}');
// Imagine a vulnerable merge function here
merge(myObj, untrustedInput);
// Now, every object in the system has isAdmin = true
let guest = {};
console.log(guest.isAdmin); // true
How Prototype Pollution Vulnerabilities Occur
There are three primary coding patterns that typically introduce prototype pollution into a JavaScript application. These patterns are often found in utility libraries used for object manipulation, such as older versions of Lodash or jQuery.
1. Recursive Merging
A recursive merge function takes properties from a source object and copies them to a target object. If the function does not explicitly block the __proto__ or constructor keys, an attacker can provide a payload that navigates to the prototype.
function merge(target, source) {
for (let key in source) {
if (typeof target[key] === 'object' && typeof source[key] === 'object') {
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
If source contains {"__proto__": {"polluted": "yes"}}, the call target["__proto__"] will return the global prototype, and the function will then set Object.prototype.polluted = "yes".
2. Property Assignment by Path
Some applications allow setting object properties using a path string, such as set(user, "profile.name", "Bob"). If the path is user-controlled, an attacker can provide a path like __proto__.role to inject a global property.
3. Object Cloning
Similar to merging, cloning an object by iterating through its keys and values can lead to pollution if the implementation doesn't account for the special nature of the __proto__ property during the deep copy process.
Exploitation Scenarios and Payloads
The impact of prototype pollution depends heavily on how the application uses objects. We generally categorize exploitation into client-side and server-side scenarios.
Client-Side: Cross-Site Scripting (XSS)
In client-side JavaScript, prototype pollution is frequently used to achieve DOM-based XSS. Many frontend frameworks and libraries use configuration objects to initialize components. If an attacker can pollute a property that is later used to render HTML or execute scripts, they can hijack the execution flow.
Imagine a library that renders a button based on a config object:
// Vulnerable code in a library
function renderButton(config) {
const scriptUrl = config.scriptUrl || "/default-button.js";
const script = document.createElement('script');
script.src = scriptUrl;
document.body.appendChild(script);
}
If the attacker pollutes Object.prototype.scriptUrl with a malicious URL via a vulnerable URL search parameter parser, the config.scriptUrl check will find the polluted value even if the config object itself is empty. This results in the execution of an external malicious script.
Server-Side: Remote Code Execution (RCE)
In Node.js environments, prototype pollution can be even more devastating. It can lead to RCE by affecting how system commands are executed or how templates are rendered.
For example, the child_process.spawn function in Node.js can take an options object. If an attacker can pollute Object.prototype.shell or Object.prototype.env, they can influence how the process is spawned.
// Payload targeting Node.js child_process
{
"__proto__": {
"shell": "node",
"NODE_OPTIONS": "--inspect-brk=0.0.0.0:1337"
}
}
If the application later calls spawn('ls') without explicitly defining the shell option, it will inherit the polluted shell property, potentially allowing the attacker to execute arbitrary commands or open a debugging port.
Another common server-side target is template engines like EJS or Pug. These engines often use internal options objects to manage compilation. By polluting properties like client, escape, or block, attackers can inject code into the template compilation process, leading to code execution when the page is rendered.
Impact on Modern Infrastructure
Prototype pollution is a "silent" vulnerability. It doesn't always cause an immediate crash or obvious error. Instead, it changes the environment in which the code runs. This makes it particularly dangerous for Jsmon users and security professionals because it can bypass traditional security controls that only look for direct input-to-sink flows.
As organizations move toward microservices and complex JavaScript-heavy stacks, a single vulnerable dependency in a shared library can expose the entire infrastructure. The impact ranges from:
- Data Leakage: Overwriting properties that control data access or API endpoints.
- Authentication Bypass: Polluting properties like
isAdminorauthenticatedin session objects. - Denial of Service (DoS): Polluting properties used in loops or recursion, causing the application to hang or crash.
How to Prevent Prototype Pollution
Preventing prototype pollution requires a combination of secure coding practices and the use of modern JavaScript features.
1. Use Map Instead of Objects
When you need a key-value store for user-provided data, use the Map data structure instead of a plain JavaScript object. Map does not have a prototype chain in the same way, making it immune to prototype pollution.
const data = new Map();
data.set(untrustedKey, untrustedValue);
2. Create Objects Without Prototypes
If you must use an object, create it using Object.create(null). This creates an object that does not inherit from Object.prototype, meaning it has no __proto__ property to target.
const cleanObj = Object.create(null);
3. Freeze the Prototype
In your application's entry point, you can freeze the Object.prototype to prevent any further modifications. This is a global defensive measure.
Object.freeze(Object.prototype);
Object.freeze(Array.prototype);
4. Schema Validation
Always validate incoming JSON payloads against a strict schema. Use libraries like Ajv or Joi and ensure that you explicitly disallow keys like __proto__, constructor, and prototype.
5. Use Secure Merge Libraries
If you need to merge objects, ensure you are using the latest versions of libraries like Lodash (v4.17.15 or higher), which have implemented protections against prototype pollution.
Conclusion
Prototype pollution is a powerful reminder that the flexibility of a language can often be its greatest security weakness. By understanding the prototype chain and identifying vulnerable patterns like recursive merges and path-based assignments, developers can build more resilient applications. As we've seen, the shift from a minor logic bypass to full RCE is often just one polluted property away.
To proactively monitor your organization's external attack surface and catch exposures before attackers do, try Jsmon.