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.