August 13, 2026
Prototype Pollution: From JavaScript Object Mutation to Real Security Impact
One JSON key. Every object in your Node.js process inherits the payload. The gadget decides whether that's a crash, an auth bypass, or aโฆ

By CYBER MIND SPACE
2 min read
One JSON key. Every object in your Node.js process inherits the payload. The gadget decides whether that's a crash, an auth bypass, or a root shell.
Kibana. 2019. A researcher sends a single POST request with a JSON body that starts with __proto__.
CVE-2019-7609. CVSS 10.0. Prototype pollution in Kibana leading to arbitrary code execution via child_process.spawn as a gadget.
No authentication bypassed. No memory corrupted. No binary exploited. The attacker wrote one property into JavaScript's global prototype chain, found a code path that read it, and Node.js executed their command.
That's what prototype pollution looks like when it lands. And it still ships โ Lodash, one of the most downloaded npm packages on earth, patched a new prototype pollution CVE in 2025. The fix was incomplete. Another CVE was filed in 2026.
๐งฌ Why JavaScript Is Uniquely Vulnerable โ 60 Seconds
Every JavaScript object has a prototype. When you access a property that doesn't exist on an object, the engine walks up the prototype chain looking for it. At the top of every chain: Object.prototype. Every object in the entire process inherits from it.
THE PROTOTYPE CHAIN
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const user = { name: "anand" }
const admin = { role: "admin" }
Both inherit from Object.prototype
user.__proto__ === Object.prototype โ
admin.__proto__ === Object.prototype โ
If you write to Object.prototype:
Object.prototype.isAdmin = true
Then:
user.isAdmin โ true โ inherited, not set explicitly
admin.isAdmin โ true โ same
({}).isAdmin โ true โ any new object tooTHE PROTOTYPE CHAIN
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const user = { name: "anand" }
const admin = { role: "admin" }
Both inherit from Object.prototype
user.__proto__ === Object.prototype โ
admin.__proto__ === Object.prototype โ
If you write to Object.prototype:
Object.prototype.isAdmin = true
Then:
user.isAdmin โ true โ inherited, not set explicitly
admin.isAdmin โ true โ same
({}).isAdmin โ true โ any new object tooThat's prototype inheritance working as designed. The problem is JavaScript also lets you write to the prototype chain through object keys โ specifically through __proto__, constructor, and prototype.
If any code in your application takes user-controlled input and merges it into an object without filtering these keys, an attacker can write to Object.prototype directly.
๐ The Pollution Primitive โ Where It Enters
The vulnerability lives in any function that recursively merges objects without sanitizing keys.
// VULNERABLE deep merge โ present in old lodash, jQuery, countless utilities
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
target[key] = target[key] || {}
merge(target[key], source[key]) // โ no key validation
} else {
target[key] = source[key]
}
}
}
// Attacker sends:
const payload = JSON.parse('{"__proto__": {"isAdmin": true}}')
merge({}, payload)
// Result:
Object.prototype.isAdmin // โ true
// Every object in the process now has isAdmin = true// VULNERABLE deep merge โ present in old lodash, jQuery, countless utilities
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
target[key] = target[key] || {}
merge(target[key], source[key]) // โ no key validation
} else {
target[key] = source[key]
}
}
}
// Attacker sends:
const payload = JSON.parse('{"__proto__": {"isAdmin": true}}')
merge({}, payload)
// Result:
Object.prototype.isAdmin // โ true
// Every object in the process now has isAdmin = trueThe three keys that matter:
__proto__ โ directly walks to Object.prototype
constructor โ accesses the object's constructor function
constructor.prototype โ same destination, different path
All three lead to the same write primitive.__proto__ โ directly walks to Object.prototype
constructor โ accesses the object's constructor function
constructor.prototype โ same destination, different path
All three lead to the same write primitive.JavaScript allows all Object attributes to be altered, including their magical attributes such as proto, constructor and prototype. Properties on Object.prototype are then inherited by all the JavaScript objects through the prototype chain โ leading to either denial of service by triggering JavaScript exceptions, or remote code execution by forcing code paths the attacker injects.
๐ฅ๏ธ Client-Side Pollution โ DOM XSS
On the browser side, prototype pollution usually surfaces through URL query string parsing.
ATTACK URL
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
https://app.com/page?__proto__[innerHTML]=<img src=x onerror=alert(1)>
Vulnerable query parser:
const params = parseQueryString(location.search)
// Writes: Object.prototype.innerHTML = "<img src=x onerror=...>"
Gadget โ any code that reads innerHTML from an object:
document.getElementById("app").innerHTML = options.innerHTML
โ
reads undefined prop โ walks prototype chain
โ gets attacker's value โ XSS firesATTACK URL
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
https://app.com/page?__proto__[innerHTML]=<img src=x onerror=alert(1)>
Vulnerable query parser:
const params = parseQueryString(location.search)
// Writes: Object.prototype.innerHTML = "<img src=x onerror=...>"
Gadget โ any code that reads innerHTML from an object:
document.getElementById("app").innerHTML = options.innerHTML
โ
reads undefined prop โ walks prototype chain
โ gets attacker's value โ XSS firesThe qs library had this. jQuery's $.extend had this. query-string had this. All patched โ but the pattern recurs in every library that parses user-supplied key-value pairs into nested objects.
Client-side pollution is dangerous. Server-side is worse.