What is Prototype Pollution?
Prototype pollution is a JavaScript vulnerability where an attacker can inject properties into Object.prototype — the base prototype from which all JavaScript objects inherit. Because properties on Object.prototype are accessible on every plain object, polluting it can change the behavior of the entire application, override security checks, and in some environments lead to Remote Code Execution.
The vulnerability commonly appears in deep merge, deep clone, or recursive property assignment utilities when they process attacker-controlled JSON without checking for dangerous keys like __proto__, constructor, or prototype.
How exploitation works
A deep merge function copies properties from a source object to a target without checking for __proto__:
function merge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object') {
merge(target[key] || (target[key] = {}), source[key]);
} else {
target[key] = source[key];
}
}
}
// Attacker sends: { "__proto__": { "isAdmin": true } }
merge({}, JSON.parse(attackerInput));
// Now EVERY object inherits isAdmin: true
console.log({}.isAdmin); // true — security check bypassed
In Node.js with template engines like Handlebars or Pug, prototype pollution can escalate to RCE by injecting properties that influence template compilation.
Vulnerable code examples
Custom deep merge
// VULNERABLE: No key sanitization — processes __proto__ as a regular key
function deepMerge(dst, src) {
Object.keys(src).forEach(key => {
if (typeof src[key] === 'object' && src[key] !== null) {
deepMerge(dst[key] = dst[key] || {}, src[key]);
} else {
dst[key] = src[key];
}
});
return dst;
}
// Vulnerable call
app.post('/settings', (req, res) => {
const config = deepMerge(defaultConfig, req.body); // req.body is attacker-controlled
});
Vulnerable lodash (pre-4.17.5)
// VULNERABLE: Older lodash _.merge() was susceptible to prototype pollution
const _ = require('lodash'); // version < 4.17.5
_.merge({}, JSON.parse(userInput));
Secure code examples
Safe merge with key allowlist
// SECURE: Block dangerous prototype keys explicitly
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
function safeMerge(dst, src) {
Object.keys(src).forEach(key => {
if (FORBIDDEN_KEYS.has(key)) return; // Skip dangerous keys
if (typeof src[key] === 'object' && src[key] !== null) {
safeMerge(dst[key] = dst[key] || Object.create(null), src[key]);
} else {
dst[key] = src[key];
}
});
return dst;
}
Use null-prototype objects for untrusted data
// SECURE: Objects without prototype cannot pollute Object.prototype
function processUserSettings(userInput) {
const parsed = JSON.parse(userInput);
// Create storage with no prototype chain
const safe = Object.assign(Object.create(null), parsed);
// Process safe without risk of inherited pollution
return safe;
}
What Offensive360 detects
- Unsafe deep merge/clone — Recursive object property assignment without
__proto__,constructor, orprototypekey checks - Bracket-notation assignment with user keys —
obj[userKey] = valuepatterns whereuserKeyis tainted - Unpatched utility library versions — Known-vulnerable versions of lodash, jQuery, or minimist used in package.json
Object.assignwith nested user objects — Shallow assign is safe, but combined with other operations can still be exploited
Remediation guidance
-
Block dangerous prototype keys — In any recursive merge or property assignment, explicitly reject
__proto__,constructor, andprototypeas keys. -
Use
Object.create(null)for data storage — Objects created with a null prototype have no prototype chain and cannot polluteObject.prototype. -
Use
hasOwnPropertychecks — When iterating object keys, useObject.prototype.hasOwnProperty.call(obj, key)rather thaninoperator or inherited enumeration. -
Keep dependencies updated — Prototype pollution was patched in lodash 4.17.5+, jQuery 3.4.0+, and minimist 0.2.1+. Audit and update regularly.
-
Freeze Object.prototype —
Object.freeze(Object.prototype)prevents property injection at runtime. Test thoroughly for compatibility with third-party libraries.