Security Cipher

Security Resources

⌘K
  1. Home
  2. Security Resources
  3. Vulnerability Explain
  4. Prototype Pollution

Prototype Pollution

This Vulnerability Explain post covers Prototype Pollution, a JavaScript-specific flaw that has caused serious issues across the Node.js ecosystem. By polluting the base object prototype, attackers can change application behavior, bypass logic, and sometimes reach remote code execution. Let’s break it down.

What is Prototype Pollution?

Prototype Pollution is a vulnerability in JavaScript where an attacker injects properties into Object.prototype – the prototype that nearly every object inherits from. Once polluted, those properties appear on all objects, changing application behavior globally.

It typically happens when code merges or sets nested properties from untrusted input using keys like __proto__, constructor, or prototype. For example, a deep-merge of user JSON containing {“__proto__”: {“isAdmin”: true}} can make every object suddenly appear to have isAdmin set to true.

How does Prototype Pollution work?

Prototype pollution spreads from one bad merge to global impact:

  • Unsafe Property Assignment
    • Code recursively merges or sets nested keys from user input without filtering special keys.
  • Special Keys Injected
    • The attacker supplies __proto__, constructor, or prototype in the input.
  • Prototype Modified
    • The assignment reaches Object.prototype and adds or changes a property.
  • Global Effect
    • Every object now inherits the polluted property, altering checks and defaults across the app.
  • Impact
    • Outcomes range from authentication and logic bypass to denial of service and, in some sinks, remote code execution.

Prototype pollution is uniquely dangerous because one tainted merge changes the behavior of the entire runtime. Blocking the magic keys and avoiding unsafe merges is the core defense.

Prototype pollution diagram showing a __proto__ payload modifying the base JavaScript object prototype

How prototype pollution works: a __proto__ payload modifies the shared Object prototype.

Tools and Techniques for Prototype Pollution Testing

Testing involves injecting prototype keys and observing changed behavior or known sinks.

Manual Testing Methodologies

  • Magic Key Injection – Send payloads containing __proto__, constructor, and prototype in JSON bodies and query parameters.
  • Behavior Probing – After injection, check whether default values or access checks change across the app.
  • Client-Side Testing – Test front-end libraries that merge URL or input data into objects.
  • Gadget Hunting – Identify code paths (sinks) where a polluted property leads to a stronger impact.

Automated Scanning Tools

Prototype Pollution Protection Mechanisms

Best Practices for Secure Coding

  • Block Dangerous Keys
    • Description: Reject or strip __proto__, constructor, and prototype from untrusted input.
    • Benefits: Stops the injection at the source.
    • Implementation Tip: Validate keys before any merge or assignment.
  • Avoid Unsafe Merges
    • Description: Do not recursively merge untrusted data into existing objects.
    • Benefits: Removes the most common sink.
    • Implementation Tip: Use Map for key-value data, or Object.create(null) for dictionaries.
  • Freeze and Validate
    • Description: Freeze prototypes where feasible and validate input against a schema.
    • Benefits: Hardens objects against modification.
    • Implementation Tip: Use Object.freeze(Object.prototype) cautiously and schema validation libraries.

Best Practices for Organizations

  • Dependency Hygiene
    • Keep libraries patched, since many merge utilities had pollution bugs.
    • Audit dependencies with SCA tools.
  • Safe Defaults
    • Prefer null-prototype objects and Maps for untrusted data.
    • Provide a vetted safe-merge utility.
  • Testing
    • Add prototype pollution tests to CI.
    • Re-test after adopting new merge or parsing libraries.

Top Prototype Pollution payloads used by Security Researchers

As a security researcher, knowing the most common payloads helps you detect and prevent these attacks. Use this knowledge ethically and only on systems you are authorized to test. Some sample payloads are shown below.

// JSON body polluting Object.prototype
{ "__proto__": { "isAdmin": true } }
// Constructor-based variant
{ "constructor": { "prototype": { "isAdmin": true } } }
// Query-string nested pollution
?a[__proto__][polluted]=yes
// Effect after pollution (any empty object now inherits it)
({}).isAdmin === true  // true once polluted

Real-World Example: Auth Bypass via __proto__

A Node.js app deep-merged the JSON request body into a configuration object using a vulnerable merge helper, and elsewhere checked user.isAdmin to gate admin features.

An attacker sent {“__proto__”: {“isAdmin”: true}}. The merge wrote isAdmin onto Object.prototype, so every object – including the user object that lacked its own isAdmin field – now inherited isAdmin === true, granting admin access across the app.

The fix replaced the merge helper, stripped __proto__ and constructor keys from input, and used null-prototype objects for untrusted data. Prototype pollution proves that in JavaScript, one unsafe merge can quietly rewrite the behavior of everything.

Vulnerable and secure code of Prototype Pollution

The following example shows the contrast between vulnerable and secure code for Prototype Pollution. It helps you see how the flaw creeps into real code and the changes that shut it down.

🥺 Vulnerable Code:

// Vulnerable: recursive merge of untrusted input
function merge(target, source) {
  for (const key in source) {
    if (typeof source[key] === "object") {
      target[key] = merge(target[key] || {}, source[key]);
    } else {
      target[key] = source[key];   // key may be __proto__
    }
  }
  return target;
}
merge({}, JSON.parse(req.body));   // { "__proto__": { "isAdmin": true } }
  • The merge follows a __proto__ key straight onto Object.prototype.
  • After pollution, every object inherits the injected property like isAdmin.

😎 Secure Code:

// Secure: block dangerous keys and use null-prototype objects
const FORBIDDEN = new Set(["__proto__", "constructor", "prototype"]);

function safeMerge(target, source) {
  for (const key of Object.keys(source)) {
    if (FORBIDDEN.has(key)) continue;          // never merge magic keys
    if (source[key] && typeof source[key] === "object") {
      target[key] = safeMerge(target[key] || Object.create(null), source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}
safeMerge(Object.create(null), JSON.parse(req.body));
  • The forbidden keys __proto__, constructor, and prototype are skipped during merge.
  • Using Object.create(null) means objects have no inherited prototype to pollute.

Conclusion

Prototype Pollution is a quintessential JavaScript footgun: a single unsafe merge of untrusted input can rewrite the behavior of every object in the runtime. Block __proto__, constructor, and prototype keys, avoid recursively merging untrusted data, prefer Maps and null-prototype objects, and keep merge libraries patched. Guard the prototype chain and you guard the whole application.

How can we help?