Prototype pollution in JavaScript apps
Unsafe deep merges of untrusted JSON, __proto__ payloads, and safer patterns for frontend state and config merging.
Prototype pollution lets an attacker poison Object.prototype through unsafe merges of JSON. Every object then inherits unexpected properties. Frontend apps hit this when they deep-merge query params, config, or API payloads into plain objects.
Impact ranges from broken UI logic to gadget chains that become XSS in older utility stacks. Lodash merge incidents are the textbook case. The pattern still appears in homemade merge helpers.
How a frontend merge goes wrong
User-controlled JSON contains __proto__ or constructor.prototype. A recursive assign copies it onto the shared prototype.
- Do not deep-merge untrusted JSON into app state blindly.
- Keep dependencies updated when merge utilities are involved.
- Use Maps or null-prototype objects for untrusted key spaces.
- Prefer structured cloning or explicit field picking over recursive merge.
// Vulnerable sketch
export function unsafeMerge(target: any, source: any) {
for (const key of Object.keys(source)) {
if (source[key] && typeof source[key] === "object") {
target[key] ??= {};
unsafeMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// Payload idea after JSON.parse:
// { "__proto__": { "isAdmin": true } }
const BLOCKED = new Set(["__proto__", "prototype", "constructor"]);
function isPlainObject(value: unknown): value is Record<string, unknown> {
return (
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
Object.getPrototypeOf(value) === Object.prototype
);
}
export function safeMerge(
target: Record<string, unknown>,
source: Record<string, unknown>
) {
for (const [key, value] of Object.entries(source)) {
if (BLOCKED.has(key)) continue;
if (isPlainObject(value)) {
const current = target[key];
const next = isPlainObject(current)
? current
: Object.create(null);
target[key] = safeMerge(next, value);
} else {
target[key] = value;
}
}
return target;
}
// Even better for untrusted key spaces: use Map, or skip deep merge entirely.