Prototype Pollution
Also known as: Prototype pollution, Object prototype pollution, __proto__ pollution
A JavaScript flaw in which untrusted keys reach an object's prototype through an unguarded recursive merge or clone, changing the defaults every object inherits; it is prevented by rejecting dangerous keys and merging into prototype-free structures.
How it works
Prototype pollution is a flaw in JavaScript applications in which untrusted input changes the shared prototype that ordinary objects inherit from. Because almost every object falls back to that prototype for properties it does not define itself, one tainted entry can change the default behaviour of unrelated code across the whole process.
The usual cause is a function that recursively merges, clones or assigns properties from untrusted JSON into another object without checking the key names. Special keys that address an object's prototype are treated like any other key, so the recursion walks into the prototype and writes there. Custom deepMerge helpers and old versions of popular utility libraries are the classic sources.
The impact depends on what the application later reads from a plain object. Code that checks a flag such as an admin or debug property on an object that never set it can be influenced, and in some server-side setups the same weakness can reach template or process options. It is a flaw in how data becomes structure, so it rarely shows up in functional testing.
Defence is layered. Reject or strip the dangerous keys during any merge, build lookup structures with Map or a null-prototype object, schema-validate input to an allow-list of fields, update libraries that have prototype-pollution fixes, and freeze the base prototype where the platform allows. Dependency auditing in CI finds known vulnerable versions before release.
Walk through it
- 1Read the recursive merge
- 2Name the keys to block
- 3Pick the safer structure
- Scope every merge and clone path
- Guard, freeze and verify
A pre-release review flags the profile-settings endpoint. It merges the JSON body a user submits into their stored settings object. Read the helper as a reviewer and note what it does with every key it is given.
1// Used by PATCH /api/settings with the parsed request body2export function deepMerge(target: any, source: any): any {3 for (const key in source) {4 if (typeof source[key] === "object" && source[key] !== null) {5 if (!target[key]) target[key] = {};6 deepMerge(target[key], source[key]);7 } else {8 target[key] = source[key];9 }10 }11 return target;12}Spot it
- Dependency audit or software composition analysis flags a prototype-pollution advisory in a merge, clone or query-parsing library.
- Empty objects in logs or debuggers show properties that no code path sets.
- Authorization, debug or feature flags read as set for sessions that were never granted them.
- Request bodies containing keys named __proto__, constructor or prototype in nested JSON, seen in application or WAF logs.
- Unexpected exceptions or behaviour changes across unrelated routes in one process after a settings or profile update.
Application log
ts=2026-10-11T09:14:02Z level=warn route=PATCH:/api/settings msg=blocked_key key=__proto__ user=u-4821
ts=2026-10-11T09:14:05Z level=warn route=PATCH:/api/settings msg=blocked_key key=constructor user=u-4821CI dependency audit
ts=2026-10-11T08:00:31Z tool=npm-audit severity=high advisory=prototype-pollution package=merge-helper fixAvailable=truegrepgrep: request logs containing the prototype-addressing key names
grep -E '"(__proto__|constructor|prototype)"' /var/log/app/requests.logExpect noise from legitimate fields named constructor in some domains; correlate with the route and the merge helper before escalating.
kqlKQL: repeated blocked-key warnings from one user
AppTraces
| where Message has "blocked_key"
| summarize hits=count() by UserId=tostring(Properties.user)
| where hits > 5Stop it
Reject dangerous keys and merge into prototype-free structures
Refuse __proto__, constructor and prototype in any recursive merge, clone or path-assignment helper. Prefer a Map or an object created with no prototype for data keyed by user input, so there is no shared prototype to reach.
Use maintained libraries and keep them patched
Replace hand-written deep merges with a maintained library that guards these keys, and run dependency auditing in CI so old vulnerable versions of utility packages are caught before release.
Validate input against a schema and harden the runtime
Accept only the fields the endpoint expects, with types and nesting limits, and freeze the base prototype where the platform allows so a missed flaw cannot change inherited defaults.
Guard the keys and merge into a prototype-free object
Vulnerable
for (const key in source) {
if (typeof source[key] === "object" && source[key] !== null) {
if (!target[key]) target[key] = {};
deepMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}Hardened
const BLOCKED = new Set(["__proto__", "constructor", "prototype"]);
export function safeMerge(target: Record<string, unknown>, source: Record<string, unknown>) {
for (const key of Object.keys(source)) {
if (BLOCKED.has(key)) continue;
const value = source[key];
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
const child = Object.hasOwn(target, key) ? (target[key] as Record<string, unknown>) : Object.create(null);
target[key] = safeMerge(child, value as Record<string, unknown>);
} else {
target[key] = value;
}
}
return target;
}Own-key iteration, a block list and null-prototype children together remove the path to the shared prototype.
Use a Map for data keyed by user input
Vulnerable
const prefs: any = {};
prefs[userKey] = userValue;Hardened
const prefs = new Map<string, string>();
prefs.set(userKey, userValue);A Map stores keys as entries, never as properties on an inherited object.
- Ban hand-written recursive merge and clone helpers in coding standards; use a maintained library that guards the dangerous keys.
- Run npm audit or software composition analysis in CI and fail the build on prototype-pollution advisories.
- Validate request bodies against a schema that allow-lists fields, types and nesting depth before any merge.
- Create lookup objects with Object.create(null) or use Map when keys come from users.
- Use Object.hasOwn or hasOwnProperty for flag checks instead of reading inherited properties.
- Freeze the base prototype at startup where compatible, and consider Node's disable-proto option for the process.
If it already happened
Add a request-level block on the prototype-addressing key names at the gateway or middleware, and disable the affected settings endpoint if abuse is ongoing.
Replace the vulnerable merge helper and update affected dependencies, then restart the service processes so any polluted in-memory state is discarded.
Review sessions and records that were authorized while the flaw was live, revoke anything granted unexpectedly, and confirm behaviour with regression tests.
Add a static-analysis rule and dependency audit gate, add schema validation to every endpoint that merges input, and keep the blocked-key log detection permanently.
Check yourself
1. What is the root cause of prototype pollution in a Node service?
2. Which keys should a safe merge or clone helper refuse?
3. Which design choice most directly removes the shared prototype from the risk?