Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Prototype Pollution

Overview

Prototype pollution is a JavaScript vulnerability where an attacker modifies the prototype of Object, Array, or other built-in types, causing unexpected behavior across the entire application. It targets JavaScript’s prototype-based inheritance chain.

The Mechanism

Every JavaScript object has an internal [[Prototype]] link to Object.prototype. If an attacker can control the key used in a recursive merge (deep merge), they can write to Object.prototype using the __proto__ property.

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

// Attack payload
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, malicious);

// Every new object now inherits isAdmin
console.log(({}).isAdmin); // true

Attack Vectors

VectorEntry PointExample
Object.assignRecursive merge on attacker inputObject.assign({}, JSON.parse(userInput))
JSON.parse + deep mergeAPI body, URL query paramsPOST with nested __proto__
lodash.merge (pre-4.17.12)Any merge of untrusted data_.merge(config, req.body)
_.zipObjectDeepKeys with .prototype path_.zipObjectDeep(['a.__proto__.x'], [1])

Prevention

Primary: Never merge untrusted input recursively

// SAFE: shallow merge — __proto__ becomes a regular key
const result = { ...config, ...userInput };

// SAFE: use Object.create(null) for clean objects
const clean = Object.create(null); // no prototype chain

Secondary defenses

// Validate keys before merging
function safeMerge(target, source) {
    for (const key of Object.keys(source)) {
        if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
            continue; // skip dangerous keys
        }
        target[key] = source[key];
    }
}

// Use Map instead of plain objects for config
const config = new Map();
config.set('key', 'value'); // no prototype chain

Tooling

  • Lodash: update to >= 4.17.12 (patched _.merge)
  • Linter: eslint-plugin-security detects __proto__ usage
  • Sandbox: use Object.freeze(Object.prototype) in sensitive contexts

Interview Questions

Q: Why does prototype pollution work in JavaScript? A: JavaScript uses prototype-based inheritance. The special __proto__ property provides a writable reference to an object’s prototype. If untrusted input reaches a recursive merge, __proto__ as a key modifies Object.prototype instead of creating a regular property, affecting all objects in the runtime.

Q: How can prototype pollution lead to RCE? A: If the application uses template engines (like Pug/EJS) or child process spawning, polluted prototype properties can inject template directives or alter command arguments. For example, polluting Object.prototype.shell to /bin/sh before a child_process.exec call can achieve remote code execution.

References