Skip to main content

Free 30-min security demo Book Now

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

Put it into practice

Find Prototype Pollution (JavaScript) in your own code

Offensive360 SAST traces the tainted data flow behind this class of bug across 60+ languages and ships a secure fix with every finding. Public open-source projects scan free; teams can see it on their own repository in a 30-minute demo.