Skip to main content

Free 30-min security demo  — We'll scan your real code and show live findings, no commitment Book Now

Offensive360
Academy Prototype Pollution (JavaScript)
Advanced · 20 min

Prototype Pollution (JavaScript)

Discover how attackers poison JavaScript's prototype chain to add properties to all objects — and how to write merge functions that cannot be exploited.

1 What is Prototype Pollution?

Prototype Pollution is a JavaScript-specific vulnerability where an attacker can add or modify properties on Object.prototype — the root prototype that all plain JavaScript objects inherit from. Once polluted, every object in the application inherits the injected properties.

How JavaScript prototypes work:

const obj = {};
console.log(obj.isAdmin);  // undefined — property doesn't exist

// If an attacker pollutes Object.prototype:
Object.prototype.isAdmin = true;

// Now EVERY plain object inherits it:
const user = {};
console.log(user.isAdmin);  // true — without it ever being set on user!

Vulnerable deep merge function (the classic source):

function merge(target, source) {
    for (let key in source) {
        if (typeof source[key] === 'object') {
            merge(target[key], source[key]);  // recurse
        } else {
            target[key] = source[key];        // assign
        }
    }
    return target;
}

// Attacker provides this JSON as source:
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, malicious);

// Now every object in the app thinks it is an admin:
console.log({}.isAdmin);  // true

When key is __proto__ and the code does target[key] = source[key], it sets a property directly on Object.prototype, polluting every subsequent object.

2 Exploitation via __proto__ and constructor.prototype

Attackers exploit prototype pollution through two primary vectors: __proto__ and constructor.prototype. Both provide access to the prototype chain during property assignment.

Via __proto__ in JSON input:

// Malicious JSON sent to any endpoint that deep-merges user input:
{
  "__proto__": {
    "isAdmin": true,
    "role": "admin",
    "polluted": "yes"
  }
}

Via constructor.prototype:

// Alternative path to Object.prototype:
{
  "constructor": {
    "prototype": {
      "isAdmin": true
    }
  }
}

Bypassing hasOwnProperty checks:

// Attacker pollutes hasOwnProperty itself:
Object.prototype.hasOwnProperty = () => true;

// Now even this "safe" check is bypassed:
const obj = {};
obj.hasOwnProperty('isAdmin');  // returns true even though isAdmin doesn't exist on obj

Prototype pollution in popular libraries: CVEs have been found in lodash's merge/defaultsDeep, jQuery's $.extend with deep=true, Hoek, minimist, y18n, and many others. Always keep dependencies updated and check npm audit regularly.

Impact chain: pollution → security bypass (isAdmin check) → privilege escalation → RCE (if template engines or child_process arguments read prototype-inherited values).

3 Fixing with Object.create(null) and Validation

There are several complementary defenses against prototype pollution:

1. Block dangerous keys in merge functions:

const BLOCKED_KEYS = new Set(['__proto__', 'constructor', 'prototype']);

function safeMerge(target, source) {
    if (typeof source !== 'object' || source === null) return target;

    for (const key of Object.keys(source)) {  // Object.keys() — no inherited props
        if (BLOCKED_KEYS.has(key)) continue;  // skip dangerous keys

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

2. Use Object.create(null) for dictionaries — no prototype at all:

// Plain objects inherit from Object.prototype — pollutable
const vulnerable = {};

// Object.create(null) has NO prototype — nothing to pollute
const safe = Object.create(null);
console.log(safe.__proto__);   // undefined — no prototype chain
console.log(safe.toString);    // undefined — doesn't inherit anything

3. Freeze Object.prototype to prevent pollution:

// At application startup, before any untrusted input is processed:
Object.freeze(Object.prototype);

// Now any attempt to set properties on Object.prototype silently fails (or throws in strict mode):
Object.prototype.isAdmin = true;  // no effect
console.log({}.isAdmin);  // undefined

4. Use JSON schema validation: Validate all incoming JSON against a strict schema before merging. Reject payloads with unexpected keys. Libraries like ajv make this straightforward.

5. Use structuredClone() or JSON.parse(JSON.stringify()) for deep copies — these do not carry prototype-polluted properties because they serialize only own enumerable properties.

6. Keep dependencies updated — run npm audit fix regularly. Most prototype pollution CVEs in popular libraries are patched quickly.

Knowledge Check

0/4 correct
Q1

What makes prototype pollution particularly dangerous compared to a regular property injection?

Q2

Why is using for...in to iterate source keys in a merge function dangerous?

Q3

What does Object.create(null) return, and why is it useful against prototype pollution?

Q4

Which code pattern in a merge function is the most direct defense against prototype pollution?

Code Exercise

Fix the Deep Merge Function Against Prototype Pollution

The deepMerge() function below is vulnerable to prototype pollution — it does not validate keys before merging, allowing an attacker to send {"__proto__": {"isAdmin": true}} and pollute Object.prototype. Fix it by blocking the dangerous keys (__proto__, constructor, prototype) and using Object.keys() instead of for...in.

javascript