Prototype Pollution in Recursive Object.assign
Prototype pollution is a critical JavaScript vulnerability where an
attacker manipulates the prototype of base objects, potentially leading
to property injection, application crashes, or remote code execution.
This article explores how custom recursive implementations of
Object.assign—commonly known as deep merge functions—can
inadvertently expose Node.js and browser applications to prototype
pollution, examine how attacks occur, and provide actionable techniques
to secure your code.
Understanding Prototype Pollution
In JavaScript, objects inherit properties and methods from prototypes
via the prototype chain. The root of this chain is usually
Object.prototype. When an application modifies
Object.prototype, the new or altered properties become
accessible on every object in the runtime environment.
Prototype pollution occurs when untrusted input is processed without
proper sanitization, allowing an attacker to inject properties into
Object.prototype using special accessor keys such as
__proto__, constructor, or
prototype.
Why Recursive Object.assign Is Vulnerable
Native Object.assign() performs a shallow copy and does
not traverse nested objects. To overcome this, developers frequently
implement recursive wrappers around Object.assign or write
custom deep merge utilities.
A vulnerable recursive merge function typically follows this pattern: 1. It iterates over the keys of a source object. 2. If a key’s value is another object, it recursively merges that object into the target. 3. If the key’s value is a primitive, it copies the value directly.
When such a function encounters a payload containing the key
__proto__, it accesses the target object’s prototype
directly. Instead of assigning a property to the intended local object,
the recursive function sets properties on Object.prototype,
polluting every object in the application.
// Vulnerable recursive merge implementation
function deepMerge(target, source) {
for (let key in source) {
if (source[key] instanceof Object && key in target) {
deepMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// Malicious payload
const maliciousPayload = JSON.parse('{"__proto__": {"isAdmin": true}}');
// Merging the payload
deepMerge({}, maliciousPayload);
// The vulnerability manifests globally
const newUser = {};
console.log(newUser.isAdmin); // truePotential Impact of Exploitation
- Denial of Service (DoS): Overwriting built-in
methods like
toStringorvalueOfwith non-function values causes the application to crash whenever those functions are invoked. - Privilege Escalation: Injecting flags such as
isAdmin,isAuthorized, orrolecan bypass authorization checks across an entire application. - Remote Code Execution (RCE): In Node.js environments, polluting properties utilized by internal child processes, template engines, or execution contexts can allow attackers to execute arbitrary shell commands.
Mitigation Strategies
To prevent prototype pollution in recursive merge operations:
Filter Dangerous Keys: Explicitly block sensitive properties such as
__proto__,constructor, andprototypebefore performing any assignment or recursive call.function safeDeepMerge(target, source) { for (let key of Object.keys(source)) { if (key === '__proto__' || key === 'constructor' || key === 'prototype') { continue; } if (typeof source[key] === 'object' && source[key] !== null) { if (!target[key]) target[key] = {}; safeDeepMerge(target[key], source[key]); } else { target[key] = source[key]; } } return target; }Use Prototype-less Objects: When storing key-value pairs from user input, create objects with no prototype using
Object.create(null)or use theMapdata structure.Freeze the Prototype: Prevent modifications to the root prototype by calling
Object.freeze(Object.prototype)at application startup. Note that this may cause compatibility issues with legacy libraries that modify prototypes legitimately.Use Validated Libraries: Avoid writing custom deep merge algorithms. Rely on well-maintained libraries like modern versions of
lodash.mergethat include built-in protection against prototype pollution.