Prototype pollution happens when code copies keys from untrusted input into an object without filtering, and one of those keys is __proto__, constructor, or prototype. Writing through them modifies Object.prototype, so the change leaks into every object in the app. Depending on what your code later reads off objects, this ranges from broken logic to authorization bypass to remote code execution. The fix is to reject those keys and merge with safe primitives.
Why it's a problem
Because almost every object inherits from Object.prototype, polluting it is a global side effect from a single request. An attacker can inject a property that your code later trusts, for example an isAdmin flag or an option that flips a dangerous code path on. The input looks like ordinary JSON, and the effect shows up far from the merge, which makes it hard to trace.
The pattern
// naive recursive merge copies whatever keys the input contains
function merge(target, source) {
for (const key in source) {
if (typeof source[key] === "object") {
target[key] = merge(target[key] || {}, source[key]);
} else {
target[key] = source[key]; // key could be "__proto__"
}
}
return target;
}
merge({}, JSON.parse(req.body));The fix
const BLOCKED = new Set(["__proto__", "constructor", "prototype"]);
function safeMerge(target, source) {
for (const key of Object.keys(source)) {
if (BLOCKED.has(key)) continue; // never copy dangerous keys
const val = source[key];
if (val && typeof val === "object" && !Array.isArray(val)) {
target[key] = safeMerge(target[key] ?? Object.create(null), val);
} else {
target[key] = val;
}
}
return target;
}
safeMerge(Object.create(null), JSON.parse(req.body));Why AI tools write this
A recursive merge is a common utility, and the straightforward version using for-in copies every enumerable key without a second thought. The assistant writes the version that passes an obvious test, and the __proto__ case never appears in normal data, so the missing guard is invisible until someone sends it deliberately.
The quick fix
- Reject or skip __proto__, constructor, and prototype keys in any merge or assign over untrusted input.
- Iterate with Object.keys and build onto Object.create(null) rather than a plain {}.
- Prefer a maintained library (lodash merge is hardened) over a hand-rolled deep merge.
- Validate input against a schema so unexpected keys are dropped before merging.
Common questions
How do I test if my app is vulnerable to prototype pollution?
Send a request with a body containing { "__proto__": { "admin": true } } and then check whether a fresh empty object has an admin property. If it does, the pollution reached Object.prototype.
Does lodash merge protect against prototype pollution?
Modern versions of lodash (4.17.21+) are patched against this. Older versions were vulnerable. Update to the patched version and avoid hand-rolled deep merges on untrusted input.
Can schema validation alone prevent prototype pollution?
It can help by rejecting unexpected keys before they reach the merge. But the schema must explicitly disallow __proto__, constructor, and prototype keys, or a schema that allows additional properties might still pass them through.