function merge(target, source) {
for (const key in source) {
if (typeof source[key] === "object" && source[key] !== null) {
target[key] = merge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
merge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));
The last line gives every object in the process isAdmin: true. This is prototype pollution.
How it happens:
JSON.parsecreates a normal own property named__proto__.- The recursive merge then writes into
target.__proto__, which isObject.prototype. - Every object inherits from
Object.prototype, so every object now hasisAdmin: true.
So a check like if (user.isAdmin) passes for any user who has no isAdmin field. Depending on the code, this can lead to authorization bypass, crashes, or even RCE (remote code execution) through polluted options.
Defences:
- Skip the keys
__proto__,constructorandprototypein merge, clone and path-setting code. - Keep libraries that do deep merges up to date.
- Validate request bodies with strict schemas that reject unknown keys.
- Use
MaporObject.create(null)for dictionaries with user-supplied keys. These have no prototype to pollute. - Check own properties with
Object.hasOwn.
- A
__proto__key in a merge writes toObject.prototype. - Every object then inherits the polluted property.
- The impact can be auth bypass, DoS (denial of service) and sometimes RCE.
- Block the
__proto__,constructorandprototypekeys in merges. - Use strict schemas,
Mapor null-prototype objects.
Read more
- Node.js Security Cheat Sheet (OWASP) (https://cheatsheetseries.owasp.org/cheatsheets/Nodejs_Security_Cheat_Sheet.html)cheatsheetseries.owasp.org
- Inheritance and the prototype chain (MDN) (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain)developer.mozilla.org