Prototype Pollution: The JavaScript Vulnerability You Might Be Ignoring
Every JavaScript object implicitly inherits from Object.prototype. That shared prototype is what gives {} methods like .toString() and .hasOwnProperty(). It also means that if an attacker can write to Object.prototype, they can inject properties into every object in the application — silently, globally, retroactively.
This is prototype pollution, and it has been exploited in real production applications through popular npm packages.
How It Works
JavaScript property lookups traverse the prototype chain. If a property is not found on an object itself, the engine checks Object.getPrototypeOf(obj), then its prototype, and so on.
const obj = {}
obj.__proto__.isAdmin = true
const user = {}
console.log(user.isAdmin) // true — inherited from Object.prototype
Adding a property to Object.prototype makes it appear on every plain object created before or after.
The Vulnerable Pattern: Deep Merge
The most common vector is a poorly written deep merge or deep clone utility. This pattern appears in hundreds of npm packages:
// ❌ Vulnerable deep merge
function merge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object' && source[key] !== null) {
if (!target[key]) target[key] = {}
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
return target
}
// Attacker-controlled JSON input
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}')
merge({}, malicious)
// Now every object is "admin"
const user = {}
console.log(user.isAdmin) // true
When key === '__proto__' and target[key] is Object.prototype, the recursive merge writes directly to the shared prototype.
Real-World Impact
Prototype pollution can lead to:
Logic bypasses: Application code that checks if (obj.isAdmin) or if (config.debug) is now true for all objects — without any authentication.
Remote code execution (server-side): In Node.js, certain template engines and serialisation libraries use prototype properties internally. Polluting Object.prototype with keys like outputFunctionName in Lodash + EJS led to RCE in 2019 (CVE-2019-10744).
Denial of service: Polluting base prototype properties (toString, constructor) can cause unexpected exceptions throughout an application.
How to Fix: Key Sanitisation
The direct fix is to check for dangerous keys before assigning:
function merge(target, source) {
for (const key in source) {
// ✅ Block prototype pollution keys
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
continue
}
if (typeof source[key] === 'object' && source[key] !== null) {
if (!target[key]) target[key] = {}
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
return target
}
How to Fix: Use Object.create(null)
For objects used as data maps (key-value stores), create them with a null prototype — they won't inherit from Object.prototype at all:
// ✅ No prototype chain — immune to pollution
const map = Object.create(null)
map['key'] = 'value'
console.log(map.__proto__) // undefined
How to Fix: hasOwnProperty Checks
When iterating objects, use Object.hasOwn() (or Object.prototype.hasOwnProperty.call()) to avoid iterating over inherited properties:
for (const key in source) {
if (Object.hasOwn(source, key)) {
// safe to use key
}
}
How to Fix: Object.freeze()
Freezing Object.prototype prevents any modifications to it:
Object.freeze(Object.prototype)
// Now assigning to __proto__ silently fails (or throws in strict mode)
This is a blunt instrument — it can break libraries that patch Object.prototype intentionally — but it's an effective defence in environments you control.
How to Detect It
The --js-flags=--harmony-proto-dot-attr flag (V8) and security scanners like Snyk's is-proto-polluted can detect pollution at runtime. In tests, assert that Object.prototype has no unexpected properties after processing untrusted input.
Using Safe Libraries
Most major libraries have patched their deep merge and clone utilities against prototype pollution. If you're using lodash.merge, deepmerge, or similar, check your installed version against known CVEs. The semver package's advisory database (via npm audit) will flag vulnerable versions.
Conclusion
Prototype pollution happens when untrusted input flows into a recursive merge, clone, or set operation without sanitising special keys like __proto__, constructor, and prototype. The fix is to explicitly block those keys, use Object.create(null) for data containers, and keep your utility library versions current. For server-side Node.js applications in particular, prototype pollution is a viable path to remote code execution — it deserves the same attention as SQL injection.
