Skip to content
>_ITDITDWeb Security Platform

Security Guides

Prototype pollution in Node.js — where the runtime says "not our vulnerability", nobody else is standing

A special key in incoming data, carried through a recursive merge, makes properties you never set appear across unrelated objects. Node.js says this is not a core vulnerability — the defense is yours.

Published 2026-09-05 Updated 2026-09-05 Last verified 2026-09-05 8 min read

You take in data from outside, and afterwards properties you never set start appearing on unrelated objects. Or a standard method like hasOwnProperty is suddenly "not a function" and the app falls over. Both are symptoms of prototype pollution.

Start with the boundary of responsibility

Node.js says outright that this is not its vulnerability

Node.js's official security guidance is explicit: "Per the Node.js threat model, prototype pollution that relies on an attacker controlling user input is not considered a vulnerability in Node.js core, because Node.js trusts the inputs provided by application code."

And immediately after: "Nonetheless, prototype pollution is a serious class of vulnerabilities for Node.js applications and third-party libraries, and you should implement defenses at the application and dependency level."

That is not a contradiction — it is a boundary being declared. The runtime is standing on the side that does not guarantee input is well-formed. Which means "wait for a CVE, wait for an update" does not work here. It is another instance of something this site keeps running into: the place you assumed somebody was guarding has nobody in it.

What goes wrong (two outcomes)

External input (JSON, query, form)

carries a special key

↓ recursive merge / deep copy / config assembly

The shared template (prototype) is rewritten

↓ and so is everything inheriting it

1. Values you never set appear

permission flags and option checks bend

2. Built-in methods vanish

the app crashes somewhere unrelated (DoS)

You think you polluted one object; the effect lands on everything sharing the template. That is why damage appears somewhere unrelated.
The two outcomes, in operational terms
1. Property injection
A value "that could not possibly be set" becomes readable. Anywhere an option object or a permission flag is decided by a mere existence check, the injected value is simply believed. Reaching an authorization decision makes it a privilege problem; reaching template or command assembly can make it considerably worse
2. Denial of service
Break the template itself and a built-in method such as hasOwnProperty raises an "is not a function" error. The documentation presents this as a potential DoS. The point worth keeping: this is not only about privilege escalation
How far it reaches
The examples cited are Node.js itself (CVE-2022-21824) and a third-party library (Lodash, CVE-2018-3721). Not writing a recursive merge yourself changes nothing if a dependency wrote one

The mitigations the documentation lists (seven)

Node.js spells them out. They are easier to apply when grouped by what they do: stop it at the entrance, make the structure unpollutable, and change how you read.

Stop it at the entrance

Avoid insecure recursive merges (the documentation cites CVE-2018-16487)
Implement JSON Schema validations for external and untrusted requests

Never letting an unexpected key in is the surest route. Reconsider designs that accept a whole deep object and merge it into existing state.

Change the structure and the reads

Object.create(null) to create objects with no prototype (ideal for dictionaries)
Object.freeze(MyObject.prototype) to freeze the prototype
・Node's --disable-proto flag to disable Object.prototype.proto
Object.hasOwn(obj, key) to check the property is the object's own
Avoid using methods from Object.prototype

The highest-leverage change is how you test for existence

In practice the broadest win is switching to Object.hasOwn(obj, key). A plain existence check — does this property exist? — answers yes for values inherited from the template too, which is exactly how an injected value gets picked up. Confirming the property is the object's own closes most of outcome 1 on its own.

For the same reason, avoid calling obj.hasOwnProperty(key) off the object itself — that method is one of the things pollution can remove (outcome 2).

What to check in your own code

1

Are you accepting external input as a deep object?

Request bodies, query strings, forms, webhook payloads — find every place that takes a whole nested structure and merges it into existing state. That is the only entrance. Deciding explicitly which keys you accept, via a schema, is the shortest fix.

2

Change how dictionary-style objects are created

Anything used as "a container whose keys are decided at runtime" (a map keyed by ID, a bag of settings) should be built with Object.create(null). With no template, there is nothing to inherit. Solve in structure what you would otherwise have to solve in validation.

3

Standardise existence checks on Object.hasOwn

Search for branches that depend on whether a property exists and convert them. Permission, flag and option checks come first — they connect directly to how authorization is implemented.

4

Put a machine on your dependencies

Not writing a recursive merge yourself changes nothing if a dependency wrote one — which is why the cited examples include a third-party library. Known vulnerabilities in dependencies cannot be tracked by hand, so put them under machine monitoring as in getting started with osv-scanner. The wider risk a poisoned dependency carries is covered in defending against npm supply-chain compromise.

5

Look at your server framework's entrance the same way

Every Node framework has a path that deserialises the request body. When we checked the NVD data, Next.js vulnerabilities clustered in server-side responsibilities — caching, middleware, Server Actions (do Next.js and React have a lot of vulnerabilities?). Start by dropping the assumption that "it's a front-end library, so this does not apply".

This site's view: a declared boundary is exactly where to look

The most operationally useful thing in this article is not the list of mitigations — it is the sentence saying this is not considered a vulnerability in Node.js core. Where a boundary is drawn clearly, there is nobody on the other side of it: no CVE will arrive, and no update will fix it.

We treat this as the same shape of problem as a certificate that does not protect you from a takeover (subdomain takeover) and a block that does not delete the existing policy underneath it (public buckets). Who is actually responsible for the place you assumed was covered? Answering that once tends to reorder your priorities considerably.

Sources (primary)

  • Node.js, "Security Best Practices" (Prototype Pollution Attacks / CWE-1321 section) — nodejs.org (the threat-model position, both outcomes, the cited CVEs and the seven mitigations all come from this document; checked 5 September 2026)

FAQ

QIs prototype pollution a Node.js bug? Will an update fix it?
A

No update is coming. Node.js's official security guidance states that per the Node.js threat model, prototype pollution that relies on an attacker controlling user input is not considered a vulnerability in Node.js core, because Node.js trusts the inputs provided by application code. It then continues: nonetheless, prototype pollution is a serious class of vulnerabilities for Node.js applications and third-party libraries, and you should implement defenses at the application and dependency level. By design, this one is yours.

QWhat actually goes wrong?
A

Two outcomes. First, properties you never set start appearing on objects throughout the process — if a permission flag or an option is decided by a mere existence check, that injected value is believed, and authorization or business logic bends. Second, denial of service: break the built-in prototype and a standard method such as hasOwnProperty raises a 'is not a function' error, crashing code that has nothing to do with the input. The second outcome is the one that corrects the assumption that this is only about privilege escalation.

QWhere do I fix it?
A

At the entrance and in the data structures. At the entrance, apply JSON Schema validation to external and untrusted requests so unexpected keys never get in. In the structures, stop using unsafe recursive merges and build dictionary-style objects with Object.create(null) so they have no prototype at all. When reading, use Object.hasOwn(obj, key) to confirm a property is the object's own, and avoid relying on methods from Object.prototype. Operationally there is also Object.freeze and Node's --disable-proto flag.

QI never wrote a recursive merge — am I safe?
A

No. The examples the documentation cites are both Node.js itself (CVE-2022-21824) and a third-party library (Lodash, CVE-2018-3721). Config merging, query-string parsing and form deserialisation are all places where widely used libraries assemble deep objects on your behalf. So the defense does not close inside your own code; it comes paired with machine-monitoring your dependencies.