0x55aa
โ† Back to Blog

Prototype Pollution: The JavaScript Vulnerability Hiding in Plain Sight ๐Ÿงชโ˜ ๏ธ

โ€ข5 min read

Prototype Pollution: The JavaScript Vulnerability Hiding in Plain Sight ๐Ÿงชโ˜ ๏ธ

Let me paint you a picture.

You've locked down your API. SQL injection? Handled. XSS? Escaped. CSRF tokens? In place. You sit back, crack your knuckles, and declare your app secure. Then some hacker sends a JSON payload with a key called __proto__ and suddenly your admin-only dashboard is open to everyone.

Welcome to prototype pollution โ€” the vulnerability that sounds made up until it absolutely isn't. ๐Ÿ˜…

What Even Is Prototype Pollution? ๐Ÿค”

JavaScript uses a prototype chain for inheritance. Every object inherits from Object.prototype, which means properties you add there magically appear on every single object in your application. That's powerful! It's also terrifying in the wrong hands.

Prototype pollution happens when an attacker can inject properties into Object.prototype (or another prototype) through a seemingly innocent operation โ€” like merging two objects.

Here's the attack in its simplest form:

// Innocent-looking user input
const userInput = JSON.parse('{"__proto__": {"isAdmin": true}}');

// Your well-intentioned deep merge utility
function deepMerge(target, source) {
  for (const key of Object.keys(source)) {
    if (typeof source[key] === 'object' && source[key] !== null) {
      if (!target[key]) target[key] = {};
      deepMerge(target[key], source[key]);
    } else {
      target[key] = source[key]; // ๐Ÿ’€ This line is the problem
    }
  }
  return target;
}

const config = {};
deepMerge(config, userInput);

// Now EVERY object in your app thinks it's an admin
const newUser = {};
console.log(newUser.isAdmin); // true ๐Ÿ˜ฑ

That target[key] = source[key] line, when key is __proto__, doesn't set a property on target โ€” it modifies the prototype of all objects. Your attacker just reached up and rewrote the laws of physics for your entire JavaScript runtime.

Real-World Impact: It Gets Worse ๐Ÿ˜ฌ

"Okay," you think, "I'll just check for isAdmin properly." But prototype pollution isn't just about boolean flags. Attackers can use it to:

  • Bypass authorization checks by polluting properties your middleware trusts
  • Cause denial of service by polluting properties that crash internal framework code
  • Achieve Remote Code Execution in Node.js apps through framework gadget chains (yes, really โ€” CVE-2019-7609 in Kibana went from prototype pollution to full RCE)

The lodash library had a critical prototype pollution vulnerability (CVE-2019-13308) affecting _.defaultsDeep and _.merge. Lodash. Used in, conservatively, half the Node.js projects on earth. Millions of apps, one bad merge.

How to Actually Fix It ๐Ÿ”ง

1. Sanitize keys before merging

function safeMerge(target, source) {
  for (const key of Object.keys(source)) {
    // Block prototype-poisoning keys
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
      continue;
    }

    if (
      typeof source[key] === 'object' &&
      source[key] !== null &&
      !Array.isArray(source[key])
    ) {
      if (!target[key]) target[key] = {};
      safeMerge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

2. Use Object.create(null) for pure data maps

// This object has NO prototype โ€” it can't be polluted
const safeMap = Object.create(null);
safeMap['user_123'] = { name: 'Alice', role: 'user' };

// __proto__ is just a regular key here, not magic
console.log(safeMap.__proto__); // undefined โ€” safe!

3. Freeze Object.prototype in your app's entrypoint

// In your server's main file, before anything else runs:
Object.freeze(Object.prototype);

// Now prototype mutation will throw in strict mode
// and silently fail in sloppy mode โ€” attackers get nothing

4. Use hasOwnProperty defensively

// Dangerous โ€” checks prototype chain
if (config.debug) { ... }

// Safe โ€” only checks the object itself
if (Object.prototype.hasOwnProperty.call(config, 'debug')) { ... }

// Or with modern syntax:
if (Object.hasOwn(config, 'debug')) { ... }

Check Your Dependencies Right Now ๐Ÿ”

Run this in any Node.js project:

# Check for known prototype pollution vulnerabilities
npm audit

# Or use a dedicated tool
npx snyk test

# Specifically look for these patterns in your codebase
grep -r "deepMerge\|_.merge\|_.defaultsDeep\|extend(" src/ --include="*.js"

And update lodash if you're on anything below 4.17.21. Seriously, do it now. I'll wait.

The Mindset Shift ๐Ÿง 

Here's what makes prototype pollution particularly nasty: the vulnerable code often looks totally fine. A deepMerge function is useful! Merging configs is normal! The attack surface is the gap between "this is valid JavaScript behavior" and "this is safe to do with untrusted input."

The fix isn't to stop merging objects โ€” it's to be paranoid about where your data comes from. Any time you're doing a recursive object merge, config load, or property assignment with keys that came from user input (query params, request bodies, JSON files from the network), you need to sanitize those keys.

Think of it like SQL injection, but for your object graph. Untrusted data should never directly control the structure of your code's internal state.

TL;DR ๐Ÿ“‹

  • Prototype pollution lets attackers modify Object.prototype, affecting every object in your app
  • It happens through unsafe recursive merges with user-controlled keys like __proto__
  • Real libraries (lodash, jQuery, many others) have had this vulnerability
  • Fix it by: blocking dangerous keys, using Object.create(null), freezing Object.prototype, and auditing your deps
  • Run npm audit today โ€” you might already be vulnerable

Got burned by prototype pollution, or found it in the wild? Share your war story on Twitter/X or connect with me on LinkedIn โ€” I collect these horror stories for educational purposes (and mild entertainment). ๐Ÿ˜„

And if you enjoyed this post, check out my other security deep-dives on SQL injection, XSS, and SSRF โ€” there's a whole universe of ways your app can betray you, and I'm documenting all of them. ๐Ÿ”