What Is Prototype Pollution and How to Prevent It

Prototype pollution is a critical JavaScript vulnerability that occurs when an attacker manipulates the base Object.prototype, injecting or modifying default properties across an entire application runtime. This article explains the mechanics behind prototype pollution, explores common attack vectors such as unsafe object merging, and provides actionable defense strategies—including prototype freezing, input sanitization, and the use of prototype-less data structures—to secure JavaScript and Node.js applications.

Understanding Prototype Pollution

JavaScript uses prototype-based inheritance. Almost all objects inherit properties and methods from a prototype chain, which terminates at Object.prototype. If an attacker successfully injects a malicious property into Object.prototype, that property becomes instantly accessible on every object created throughout the application lifecycle.

Depending on how the application handles object properties, prototype pollution can lead to serious consequences, including: * Denial of Service (DoS): Overwriting built-in methods (like toString or valueOf) to trigger uncaught exceptions. * Authentication/Logic Bypass: Injecting properties like isAdmin: true into checks that verify an object property directly. * Remote Code Execution (RCE): Overwriting sensitive system configurations or parameters passed into functions like child_process.exec.

Common Attack Vectors

Prototype pollution primarily happens when applications recursively merge, clone, or assign properties from unvalidated, user-controlled input (such as JSON payloads or URL query parameters).

A vulnerable recursive merge function allows keys like __proto__ or constructor.prototype to traverse up to the root prototype:

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

// Malicious input
const payload = JSON.parse('{"__proto__": {"polluted": true}}');
unsafeMerge({}, payload);

// The global prototype is now compromised
console.log({}.polluted); // true

How to Guard Against Prototype Pollution

Securing an application against prototype pollution requires defense-in-depth, combining safe coding patterns, runtime defenses, and strict input validation.

1. Block Sensitive Object Keys

When implementing or choosing deep clone or merge functions, explicitly reject dangerous keys such as __proto__, constructor, and prototype.

function safeMerge(target, source) {
  for (let key of Object.keys(source)) {
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
      continue; // Skip dangerous keys
    }
    if (typeof target[key] === 'object' && typeof source[key] === 'object') {
      safeMerge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

2. Use Objects Without Prototypes

For plain dictionaries or key-value storage from external input, instantiate objects using Object.create(null) instead of object literals ({}). These objects do not inherit from Object.prototype and lack the __proto__ chain entirely.

const safeDictionary = Object.create(null);
safeDictionary["anyKey"] = "value"; // Immune to Object.prototype inheritance

3. Use Map Instead of Plain Objects

When handling dynamic keys from untrusted user input, use native JavaScript Map collections rather than plain objects. Map instances do not map keys to object prototype properties, isolating user-supplied data from prototype mechanics.

const userInputs = new Map();
userInputs.set('__proto__', 'safeValue'); // Does not pollute Object.prototype

4. Freeze the Base Prototype

Freezing Object.prototype at application startup prevents existing prototype methods and properties from being modified or new properties from being added.

Object.freeze(Object.prototype);

Note: In Node.js environments, you can also launch the runtime with the --disable-proto=delete or --disable-proto=throw flag to restrict access to Object.prototype.__proto__ globally.

5. Validate Input Schemas

Use schema validation libraries (such as Zod, Joi, or Ajv) to strictly validate and sanitize payloads before processing them. Strip unrecognized properties and reject payloads that attempt to define reserved JavaScript property names.

6. Keep Dependencies Updated

Vulnerabilities often reside in third-party utilities that perform object operations (such as outdated versions of lodash or minimist). Regularly audit and patch project dependencies using tools like npm audit or automated vulnerability scanners.