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); // true2. 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); // truePrevention 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); // undefined5. 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.1Best Practices
- Use Object.create(null): For user-controlled objects
- Validate Input: Check for
__proto__,constructor,prototype - Use Safe Libraries: Lodash, deepmerge (updated versions)
- Freeze Prototypes: In critical applications
- JSON Reviver: Filter dangerous keys during parsing
- Middleware: Block requests with dangerous keys
- Regular Audits: Check dependencies for vulnerabilities
- Testing: Test for pollution in your merge/clone functions
- Code Review: Review all object manipulation code
- 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!