Front-end Engineering Lab

Prototype Pollution

Prevent prototype pollution attacks in JavaScript

Prototype Pollution

Prototype pollution is a JavaScript vulnerability where attackers modify Object.prototype, affecting all objects in your application. It can lead to RCE (Remote Code Execution) and privilege escalation.

The Problem

// ❌ DANGEROUS: User input merged into object
function merge(target, source) {
  for (let key in source) {
    target[key] = source[key];
  }
  return target;
}

// Attacker sends:
const userInput = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, userInput);

// Now ALL objects have isAdmin = true!
console.log({}.isAdmin);  // true 😱

How It Works

// JavaScript prototype chain
const obj = {};
obj.toString();  // Works! Inherited from Object.prototype

// Prototype pollution
Object.prototype.isAdmin = true;

// Now every object has isAdmin
const user = { name: 'John' };
console.log(user.isAdmin);  // true (not defined on user!)

// Dangerous in authentication
function checkAdmin(user) {
  if (user.isAdmin) {  // ❌ Always true now!
    // Grant access
  }
}

Common Attack Vectors

1. Unsafe Merge

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

// Attack
merge({}, JSON.parse('{"__proto__": {"polluted": true}}'));
console.log({}.polluted);  // true

2. Deep Clone

// ❌ VULNERABLE
function clone(obj) {
  const cloned = {};
  for (let key in obj) {
    cloned[key] = typeof obj[key] === 'object' 
      ? clone(obj[key]) 
      : obj[key];
  }
  return cloned;
}

3. Object Path Setting

// ❌ VULNERABLE
function set(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;
}

// Attack
set({}, '__proto__.polluted', true);
console.log({}.polluted);  // true

Prevention Strategies

1. Object.create(null)

// ✅ SAFE: No prototype
const safeObj = Object.create(null);
safeObj.__proto__ = { polluted: true };
console.log({}.polluted);  // undefined (not polluted!)

2. Object.hasOwnProperty

// ✅ SAFE: Check own properties only
function safeMerge(target, source) {
  for (let key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
      if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
        continue;  // Skip dangerous keys
      }
      target[key] = source[key];
    }
  }
  return target;
}

3. Blocklist Dangerous Keys

// ✅ SAFE: Block prototype keys
const dangerousKeys = ['__proto__', 'constructor', 'prototype'];

function safeMerge(target, source) {
  for (let key in source) {
    if (dangerousKeys.includes(key)) {
      continue;
    }
    target[key] = source[key];
  }
  return target;
}

4. Object.freeze

// ✅ SAFE: Freeze prototypes
Object.freeze(Object.prototype);
Object.freeze(Array.prototype);

// Now pollution attempts fail silently
Object.prototype.polluted = true;
console.log({}.polluted);  // undefined

5. Use Safe Libraries

// Lodash >= 4.17.21 (patched)
import _ from 'lodash';

// ✅ SAFE: Lodash checks for prototype pollution
_.merge({}, userInput);

// Or use safe alternative
import merge from 'deepmerge';

merge({}, userInput, {
  isMergeableObject: (value) => {
    return value && typeof value === 'object' && !Array.isArray(value);
  },
});

Safe Implementation Examples

Safe Merge

function safeMerge<T extends object, S extends object>(
  target: T,
  source: S
): T & S {
  const result = { ...target } as T & S;
  
  for (const key of Object.keys(source)) {
    // Block dangerous keys
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
      continue;
    }
    
    // Only own properties
    if (!Object.prototype.hasOwnProperty.call(source, key)) {
      continue;
    }
    
    const value = source[key as keyof S];
    
    if (value && typeof value === 'object' && !Array.isArray(value)) {
      result[key as keyof (T & S)] = safeMerge(
        (result[key as keyof T] as object) || {},
        value as object
      ) as any;
    } else {
      result[key as keyof (T & S)] = value as any;
    }
  }
  
  return result;
}

Safe Path Setter

function safeSet(
  obj: Record<string, any>,
  path: string,
  value: any
): void {
  const keys = path.split('.');
  
  // Validate path
  for (const key of keys) {
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
      throw new Error(`Dangerous key detected: ${key}`);
    }
  }
  
  let current = obj;
  
  for (let i = 0; i < keys.length - 1; i++) {
    const key = keys[i];
    
    if (!current[key] || typeof current[key] !== 'object') {
      current[key] = {};
    }
    
    current = current[key];
  }
  
  current[keys[keys.length - 1]] = value;
}

Safe Clone

function safeClone<T>(obj: T): T {
  // Use structured clone (modern browsers/Node.js)
  if (typeof structuredClone === 'function') {
    return structuredClone(obj);
  }
  
  // Fallback: JSON parse (limited but safe)
  return JSON.parse(JSON.stringify(obj));
}

JSON.parse Protection

// ✅ SAFE: Reviver removes dangerous keys
function safeParse(jsonString: string): any {
  return JSON.parse(jsonString, (key, value) => {
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
      return undefined;  // Remove dangerous keys
    }
    return value;
  });
}

// Usage
const userInput = '{"__proto__": {"isAdmin": true}, "name": "John"}';
const parsed = safeParse(userInput);
console.log({}.isAdmin);  // undefined (safe!)

Express.js Middleware

import { Request, Response, NextFunction } from 'express';

function prototypePollutionProtection(
  req: Request,
  res: Response,
  next: NextFunction
) {
  // Check request body
  if (req.body && typeof req.body === 'object') {
    const hasDangerousKeys = checkForDangerousKeys(req.body);
    
    if (hasDangerousKeys) {
      return res.status(400).json({
        error: 'Invalid request: dangerous keys detected',
      });
    }
  }
  
  next();
}

function checkForDangerousKeys(obj: any, visited = new Set()): boolean {
  if (!obj || typeof obj !== 'object' || visited.has(obj)) {
    return false;
  }
  
  visited.add(obj);
  
  const dangerousKeys = ['__proto__', 'constructor', 'prototype'];
  
  for (const key of Object.keys(obj)) {
    if (dangerousKeys.includes(key)) {
      return true;
    }
    
    if (typeof obj[key] === 'object') {
      if (checkForDangerousKeys(obj[key], visited)) {
        return true;
      }
    }
  }
  
  return false;
}

// Use middleware
app.use(express.json());
app.use(prototypePollutionProtection);

Testing for Vulnerabilities

Manual Test

// Test if your code is vulnerable
function testPrototypePollution() {
  const testObj = JSON.parse('{"__proto__": {"polluted": true}}');
  yourMergeFunction({}, testObj);
  
  const newObj = {};
  if ('polluted' in newObj) {
    console.error('❌ VULNERABLE to prototype pollution!');
  } else {
    console.log('✅ SAFE from prototype pollution');
  }
  
  // Clean up
  delete Object.prototype.polluted;
}

testPrototypePollution();

Automated Test

describe('Prototype Pollution', () => {
  afterEach(() => {
    // Clean up after each test
    delete Object.prototype.polluted;
  });
  
  it('should not pollute Object.prototype via merge', () => {
    const source = JSON.parse('{"__proto__": {"polluted": true}}');
    safeMerge({}, source);
    
    expect({}.polluted).toBeUndefined();
  });
  
  it('should reject requests with __proto__', async () => {
    const response = await fetch('/api/endpoint', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ __proto__: { polluted: true } }),
    });
    
    expect(response.status).toBe(400);
  });
});

Security Scanners

# Check dependencies for known vulnerabilities
npm audit

# Use Snyk
npx snyk test

# Check for prototype pollution specifically
npm install -g pprot
pprot analyze .

Library Updates

Many popular libraries had prototype pollution vulnerabilities:

# Update to patched versions
npm install lodash@latest        # >= 4.17.21
npm install minimist@latest      # >= 1.2.6
npm install yargs-parser@latest  # >= 18.1.2
npm install merge@latest         # >= 2.1.1

Best Practices

  1. Use Object.create(null): For user-controlled objects
  2. Validate Input: Check for __proto__, constructor, prototype
  3. Use Safe Libraries: Lodash, deepmerge (updated versions)
  4. Freeze Prototypes: In critical applications
  5. JSON Reviver: Filter dangerous keys during parsing
  6. Middleware: Block requests with dangerous keys
  7. Regular Audits: Check dependencies for vulnerabilities
  8. Testing: Test for pollution in your merge/clone functions
  9. Code Review: Review all object manipulation code
  10. Monitor: Log suspicious key usage

Common Pitfalls

Recursive merge without validation: Main attack vector
Validate keys before merging

Trusting user input: Source of pollution
Sanitize all user input

Outdated dependencies: Known vulnerabilities
Keep dependencies updated

No input validation: Accept any JSON
Validate and sanitize JSON

Using vulnerable libraries: Lodash < 4.17.21
Use patched versions

Prototype pollution is subtle but critical—validate all object operations and keep dependencies updated!

On this page