Blog · E.24 · Technical

Prototype pollution explained

In JavaScript, almost every object inherits from one shared parent - Object.prototype. So if an attacker can quietly add a property there, they've changed every object in your application at once. That's prototype pollution: a subtle-looking flaw that can bypass security checks, crash the app, or - chained to the right sink - reach remote code execution on a Node.js server. Here's how it works, what it yields, client vs server, and how to shut it down.

Prototype Pollution__proto__Node.jsDOM XSSWeb AppSec
Prototype pollution: __proto__ / constructor / prototype · Poisons Object.prototype · Affects Every Object · DoS / Property Injection / Priv-esc / RCE · Fix = Block Dangerous Keys, Null-prototype Holders Prototype pollution: __proto__ / constructor / prototype · Poisons Object.prototype · Affects Every Object · DoS / Property Injection / Priv-esc / RCE · Fix = Block Dangerous Keys, Null-prototype Holders
// TL;DR

Prototype pollution is a JavaScript flaw where an attacker manipulates a base object's prototype (usually Object.prototype) via special property names - __proto__, constructor, prototype. Because nearly every object inherits from Object.prototype, poisoning it affects every object in the app. Impact: DoS (break logic), property injection / security-check bypass (force a flag true everywhere), privilege escalation, and - chained to a dangerous sink - DOM XSS (client) or RCE (server, Node.js). Root cause: unsafe recursive merging of untrusted data into objects. Fix: reject/strip dangerous keys, use null-prototype data holders (Object.create(null)), freeze Object.prototype, use patched deep-merge libraries, validate against a schema. Detail below; part of every web app test.

// 01 The one shared parent

To understand prototype pollution you need one fact about JavaScript: almost every object inherits from a single shared parent, Object.prototype. When you access a property on an object and it's not there, JavaScript looks up the prototype chain - ultimately to Object.prototype. That inheritance is normally invisible and useful. The vulnerability: if an attacker can add or change a property on Object.prototype itself, then every object in the application appears to have that property. They do it using special property names - __proto__, constructor, prototype - in input the app processes. The classic trigger is unsafe recursive merging: code that deep-merges user-controlled JSON into an object (a config, a set of options) and, following the attacker's __proto__ key, writes onto the prototype instead of the object. One crafted request, and the pollution touches everything.

// 02 What it yields

01

Denial of service

Pollute a property that breaks core logic - every object now misbehaves, crashing or hanging the app.

02

Property injection

Change behaviour or bypass security checks - e.g. make an isAdmin flag appear true on every object.

03

Privilege escalation

Inherited attacker-controlled properties override intended access decisions.

04

XSS / RCE (chained)

When the polluted property flows into a sink: DOM XSS client-side, or RCE server-side in Node.js.

It looks subtle, but it's a genuine, sometimes critical vulnerability - not a curiosity. The severity depends on where the polluted property lands: a broken flag is a DoS; a flag that feeds an access-control decision is a bypass; one that reaches a template or process spawn is code execution.

// 03 Client-side vs server-side

Prototype pollution occurs on both sides, with the same mechanism but different worst cases. Client-side pollution happens in browser JavaScript; its headline impact is DOM-based XSS - when a polluted property reaches a dangerous DOM sink (an innerHTML, a script source, a gadget in a library), the attacker's script runs in the victim's page. Server-side pollution happens in Node.js and tends to be more severe: depending on where the polluted prototype flows, it can cause denial of service, security-control bypass, or remote code execution - for instance when a polluted property influences a template engine, a child-process spawn, or a configuration object. Because the underlying cause is identical - unsafe handling of attacker-controlled property names - both should be tested wherever JavaScript merges or copies untrusted input into objects: request bodies, query strings, JSON config, and library options.

// 04 How to test for it

Testing is targeted and gadget-aware. The tester identifies places the app merges or copies untrusted input into objects - JSON request bodies, query/parameter parsing, config merging - and submits payloads containing __proto__, constructor.prototype and similar, setting a distinctive property. Then they check whether that property has leaked onto other objects: a simple probe is to pollute a harmless property and observe whether a fresh, unrelated object now exhibits it. Confirming impact means hunting gadgets - places where a polluted property changes behaviour, reaches a DOM sink (client) or a dangerous server sink (Node.js) - to escalate from “pollution exists” to “XSS/RCE/bypass.” This is manual, JavaScript-aware work: a scanner may flag a reflected __proto__ but rarely traces the pollution to a real gadget, which is the difference between a note and a proven finding.

// 05 How to prevent it

The defence is to ensure attacker-controlled property names can never reach the prototype chain. Concretely: reject or strip dangerous keys (__proto__, constructor, prototype) during parsing and merging; use null-prototype objects - created via Object.create(null) - as data holders, so there's no prototype to pollute; freeze Object.prototype where feasible so it can't be modified; use well-maintained, patched libraries for deep-merge and object manipulation, since many prototype-pollution issues were library flaws now fixed; and validate input against a schema to reject unexpected structures. As with the other injection classes, the durable fix is structural - remove the ability for user input to write to the prototype - rather than trying to blocklist every variant. Every web app and source-review engagement we run checks the merge/copy paths for this, per our methodology.

// 06 Frequently asked questions

What is prototype pollution?

A JavaScript vulnerability where an attacker manipulates a base object's prototype (usually Object.prototype) by supplying input with special property names like __proto__, constructor or prototype. Because almost every object inherits from Object.prototype, changing a property there affects nearly every object - altering behaviour, overriding security checks, causing DoS, or leading to code execution. It typically arises from unsafe recursive merging of user-controlled data.

What can an attacker do with it?

From moderate to critical: denial of service by polluting a property that breaks logic; property injection changing behaviour or bypassing security checks (e.g. forcing an access flag true on every object); privilege escalation; and, when the polluted property reaches a dangerous sink, chaining to DOM XSS (client) or RCE (server, Node.js). It can look subtle but is a genuine, sometimes severe vulnerability.

Client-side or server-side?

Both. Client-side pollution in the browser can lead to DOM-based XSS when the polluted property reaches a dangerous sink. Server-side pollution in Node.js can be more severe - DoS, security-control bypass or RCE when the polluted prototype influences server logic or a process spawn. Same mechanism (unsafe handling of attacker-controlled property names), so test both wherever JavaScript merges untrusted input into objects.

How do you prevent it?

Handle untrusted data safely when merging/copying into objects: reject or strip dangerous keys (__proto__, constructor, prototype); use null-prototype objects (Object.create(null)) as data holders; freeze Object.prototype where feasible; use patched deep-merge libraries; and validate input against a schema. The goal is to ensure attacker-controlled property names can never reach the prototype chain.

// 07 Related reading

UG

Usama Gul

Founder & Penetration Testing Lead, CyberFortify

Traces prototype pollution from an injected __proto__ to a real gadget — DOM XSS on the client or RCE on the Node.js server — then prescribes null-prototype holders and dangerous-key rejection.

Running Node.js or heavy JS?

Anywhere your app merges untrusted input into objects can be prototype-polluted. We trace it from injection to a real gadget — DOM XSS or RCE — and hand you the structural fix.

Scope a web app test → Web app testing →