Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')
Description
Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') occurs when a product receives input specifying object attributes but does not properly control modifications of attributes of the object prototype. Attackers can inject malicious attributes via __proto__, constructor, or prototype properties to modify every object instance in the application or override critical attributes, potentially leading to denial of service, data corruption, or arbitrary code execution.
Risk
Prototype pollution has severe implications. All object instances affected. Security checks bypassed. Application data readable and modifiable. Denial of service through crashes. Remote code execution in some contexts. Authentication bypass possible. Authorization checks circumvented. Template injection enabled. Configuration tampering. High likelihood in JavaScript applications accepting nested object input.
Solution
Freeze prototypes using Object.freeze(Object.prototype) to prevent modifications. Block dangerous attributes by filtering __proto__, constructor, and prototype from user input. Use prototypeless objects via Object.create(null) which eliminates prototype access. Substitute object attribute access with Map methods. Validate untrusted objects against defined schemas before processing.
Common Consequences
| Impact | Details |
|---|---|
| Confidentiality | Scope: Confidentiality Attackers can read application data through modified prototype methods. |
| Integrity | Scope: Integrity Application data modifiable by injecting prototype attributes. |
| Availability | Scope: Availability Overriding attributes with incompatible types can cause application crashes. |
Example Code
Vulnerable Code
// Vulnerable: Recursive object merge without prototype protection
// VULNERABLE: Deep merge allows prototype pollution
function vulnerableMerge(target, source) {
for (let key in source) {
if (source.hasOwnProperty(key)) {
if (typeof source[key] === 'object' && source[key] !== null) {
if (!target[key]) {
target[key] = {};
}
// VULNERABLE: Recursively merges without checking key
vulnerableMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
return target;
}
// Attack scenario
const userInput = JSON.parse('{"__proto__": {"isAdmin": true}}');
const config = {};
vulnerableMerge(config, userInput);
// Now ALL objects have isAdmin: true
const newObj = {};
console.log(newObj.isAdmin); // true - prototype polluted!
// VULNERABLE: Property access via path
function vulnerableSetProperty(obj, path, value) {
const parts = path.split('.');
let current = obj;
for (let i = 0; i < parts.length - 1; i++) {
if (!current[parts[i]]) {
current[parts[i]] = {};
}
// VULNERABLE: Allows __proto__ traversal
current = current[parts[i]];
}
current[parts[parts.length - 1]] = value;
}
// Attack
vulnerableSetProperty({}, '__proto__.polluted', 'yes');
console.log({}.polluted); // 'yes'
// VULNERABLE: Using user input directly as object key
function vulnerableUserConfig(userId, key, value) {
const userSettings = {};
// VULNERABLE: No validation of key
userSettings[key] = value;
return userSettings;
}
// Attack: Create object with polluted prototype
vulnerableUserConfig('123', '__proto__', { admin: true });
// Vulnerable: Express.js route handler
const express = require('express');
const app = express();
app.use(express.json());
// VULNERABLE: Query string parsing
app.get('/api/search', (req, res) => {
const options = {};
// VULNERABLE: Direct assignment from query params
for (let key in req.query) {
// This allows __proto__[isAdmin]=true in query string
options[key] = req.query[key];
}
// Later code may check options.isAdmin
if (options.isAdmin) {
// Privileged operation
}
});
// VULNERABLE: Body parsing with nested objects
app.post('/api/config', (req, res) => {
const config = {};
// VULNERABLE: Deep merge with user input
vulnerableMerge(config, req.body);
// If body is {"__proto__": {"isAdmin": true}}
// All objects now have isAdmin: true
});
// VULNERABLE: Template rendering with polluted prototype
app.get('/page', (req, res) => {
const data = {};
vulnerableMerge(data, req.query);
// Template engine may use Object.prototype properties
res.render('page', data);
// Pollution can inject into template context
});
Fixed Code
// Fixed: Safe object merge with prototype pollution protection
// FIXED: Dangerous keys that should never be set
const DANGEROUS_KEYS = ['__proto__', 'constructor', 'prototype'];
function isDangerousKey(key) {
return DANGEROUS_KEYS.includes(key);
}
// FIXED: Safe deep merge
function safeMerge(target, source) {
if (!source || typeof source !== 'object') {
return target;
}
// FIXED: Use Object.keys to avoid prototype properties
for (const key of Object.keys(source)) {
// FIXED: Block dangerous keys
if (isDangerousKey(key)) {
continue; // Skip dangerous keys
}
if (typeof source[key] === 'object' &&
source[key] !== null &&
!Array.isArray(source[key])) {
if (!target[key] || typeof target[key] !== 'object') {
target[key] = {};
}
safeMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// FIXED: Safe property setter
function safeSetProperty(obj, path, value) {
const parts = path.split('.');
// FIXED: Validate all path parts
for (const part of parts) {
if (isDangerousKey(part)) {
throw new Error(`Invalid property path: ${path}`);
}
}
let current = obj;
for (let i = 0; i < parts.length - 1; i++) {
if (!current[parts[i]]) {
current[parts[i]] = Object.create(null); // FIXED: Prototypeless
}
current = current[parts[i]];
}
current[parts[parts.length - 1]] = value;
}
// FIXED: Use prototypeless objects
function createSafeObject() {
return Object.create(null); // No prototype chain
}
// FIXED: Freeze the prototype to prevent pollution
function protectGlobalPrototypes() {
Object.freeze(Object.prototype);
Object.freeze(Array.prototype);
Object.freeze(Function.prototype);
}
// FIXED: Use Map instead of object for dynamic keys
function safeUserConfig(userId, key, value) {
const userSettings = new Map();
// Map doesn't have prototype pollution issues
userSettings.set(key, value);
return userSettings;
}
// Fixed: Express.js with prototype pollution protection
const express = require('express');
const app = express();
// FIXED: Custom JSON parser that blocks dangerous keys
function safeJSONParse(json) {
return JSON.parse(json, (key, value) => {
// FIXED: Filter dangerous keys during parsing
if (isDangerousKey(key)) {
return undefined; // Remove dangerous keys
}
return value;
});
}
// FIXED: Middleware to sanitize request body
function sanitizeBody(req, res, next) {
if (req.body && typeof req.body === 'object') {
req.body = sanitizeObject(req.body);
}
next();
}
function sanitizeObject(obj, seen = new WeakSet()) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
// Prevent circular reference issues
if (seen.has(obj)) {
return obj;
}
seen.add(obj);
// FIXED: Create clean object
const clean = Array.isArray(obj) ? [] : Object.create(null);
for (const key of Object.keys(obj)) {
// FIXED: Skip dangerous keys
if (isDangerousKey(key)) {
continue;
}
clean[key] = sanitizeObject(obj[key], seen);
}
return clean;
}
app.use(express.json());
app.use(sanitizeBody);
// FIXED: Safe route handler
app.get('/api/search', (req, res) => {
// FIXED: Create prototypeless object
const options = Object.create(null);
// FIXED: Validate and copy only safe keys
for (const key of Object.keys(req.query)) {
if (!isDangerousKey(key)) {
options[key] = req.query[key];
}
}
// Safe to use options
res.json({ success: true });
});
// FIXED: Use schema validation
const Joi = require('joi');
const configSchema = Joi.object({
theme: Joi.string().valid('light', 'dark'),
language: Joi.string().max(5),
notifications: Joi.boolean()
}).unknown(false); // FIXED: Reject unknown keys
app.post('/api/config', (req, res) => {
// FIXED: Validate against schema
const { error, value } = configSchema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.message });
}
// value is now validated and safe
saveConfig(value);
res.json({ success: true });
});
// Fixed: Comprehensive protection module
class PrototypePollutionGuard {
static DANGEROUS_KEYS = new Set([
'__proto__',
'constructor',
'prototype',
'__defineGetter__',
'__defineSetter__',
'__lookupGetter__',
'__lookupSetter__'
]);
// FIXED: Check if key is dangerous
static isDangerous(key) {
return this.DANGEROUS_KEYS.has(key);
}
// FIXED: Recursively clean object
static sanitize(obj, maxDepth = 10, currentDepth = 0) {
if (currentDepth > maxDepth) {
throw new Error('Maximum object depth exceeded');
}
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(item =>
this.sanitize(item, maxDepth, currentDepth + 1)
);
}
// FIXED: Create clean prototypeless object
const result = Object.create(null);
for (const [key, value] of Object.entries(obj)) {
if (this.isDangerous(key)) {
continue;
}
result[key] = this.sanitize(value, maxDepth, currentDepth + 1);
}
return result;
}
// FIXED: Safe property access
static safeGet(obj, path) {
const parts = path.split('.');
for (const part of parts) {
if (this.isDangerous(part)) {
return undefined;
}
}
let current = obj;
for (const part of parts) {
if (current === null || current === undefined) {
return undefined;
}
if (!Object.prototype.hasOwnProperty.call(current, part)) {
return undefined;
}
current = current[part];
}
return current;
}
}
// Usage
const userInput = JSON.parse('{"__proto__": {"polluted": true}, "safe": "value"}');
const clean = PrototypePollutionGuard.sanitize(userInput);
// clean = { safe: "value" }, __proto__ removed
CVE Examples
- CVE-2019-11358: jQuery before 3.4.0 vulnerable to prototype pollution through
$.extend. - CVE-2019-10744: lodash before 4.17.12 vulnerable to prototype pollution in
_.defaultsDeep. - CVE-2020-8203: lodash before 4.17.19 vulnerable to prototype pollution in
_.zipObjectDeep. - CVE-2018-3721: lodash before 4.17.5 vulnerable to prototype pollution.
Related CWEs
- CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes (parent)
- CWE-471: Modification of Assumed-Immutable Data (can follow)
- CWE-1321: Improperly Controlled Modification of Object Prototype Attributes (self)
References
- MITRE Corporation. "CWE-1321: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')." https://cwe.mitre.org/data/definitions/1321.html
- OWASP. "Prototype Pollution Prevention Cheat Sheet"
- Snyk. "Prototype Pollution Attacks in NodeJS Applications"