ofensiva
prototype pollution
__proto__
Node.js

Prototype Pollution: JavaScript Attacks and Defense

What prototype pollution is in JavaScript and Node.js: how __proto__ leads to RCE, DoS and authentication bypass, and how to defend.

SecraJuly 6, 202610 min read

Prototype pollution is a JavaScript specific vulnerability in which an attacker injects properties into the prototype that every object in the application inherits from, usually Object.prototype. Because any plain JavaScript object resolves its properties by walking up the prototype chain, altering that base prototype has a global effect on data structures the developer never touched explicitly. Depending on the gadgets present in the code, that pollution can escalate all the way to remote code execution (RCE), denial of service (DoS) or authentication and authorization bypass.

The essentials. Prototype pollution does not exploit a memory bug or steal credentials: it abuses JavaScript's prototypal inheritance model. The attacker controls a special key (__proto__, constructor or prototype) that an insecure sink (recursive merge, deep clone, query string parser) ends up writing onto the shared prototype. The defense combines prototype less objects (Object.create(null)), freezing Object.prototype, strict key validation and the Node.js --disable-proto flag.

What prototype pollution is: the prototype chain

In JavaScript almost every plain object inherits from Object.prototype. When the engine looks up a property that does not exist as an own property, it walks up the prototype chain until it finds it or the chain runs out. That inheritance is what lets ({}).toString or ({}).hasOwnProperty work without ever being defined.

The problem arises because three keys carry a special meaning. The __proto__ accessor points directly to the object's prototype. constructor points to the constructor function, and constructor.prototype leads back to the shared prototype. If an attacker gets the application to write into obj.__proto__.something or into obj.constructor.prototype.something, they are not modifying that particular object: they are modifying the prototype that every other object in the process inherits from.

The result is that, after pollution, any object in the application that reads a missing property will receive the value injected by the attacker. A check as innocent as config.isAdmin can return true even though nobody ever wrote that property onto config.

How it happens: the vulnerable sink

Insecure recursive merge

The pattern behind most real world cases is a deep merge function that copies keys from a user controlled object into a target object without filtering dangerous keys:

function merge(target, source) {
  for (const key in source) {
    if (typeof source[key] === 'object' && source[key] !== null) {
      if (!target[key]) target[key] = {};
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
}

When key equals __proto__, the expression target['__proto__'] does not create a new property: it returns Object.prototype itself, and the recursion writes the payload directly onto it. This is the flaw that affected old versions of lodash (_.merge, _.defaultsDeep), of jQuery ($.extend(true, ...)) and of dozens of configuration utilities.

Entry vectors

A request's JSON body is the most direct vector. A body like the following, passed to a vulnerable merge, writes isAdmin onto the global prototype:

{ "__proto__": { "isAdmin": true } }

Query strings are equally dangerous when the parser expands brackets into nested objects (the default behaviour of libraries such as qs):

POST /api/update?__proto__[isAdmin]=true
POST /api/update?constructor[prototype][role]=admin

One frequent technical nuance is worth clarifying: JSON.parse('{"__proto__":{}}') creates __proto__ as an own property and does not pollute the prototype by itself. Pollution happens afterwards, when a merge or a clone copies that key using bracket notation onto the shared prototype. That is why the sink matters as much as the input.

From polluted prototype to impact

Authentication and authorization bypass

Many access controls check boolean flags through inheritance without verifying that they are own properties. If the attacker pollutes Object.prototype.isAdmin or Object.prototype.role, any session object that does not define those keys will inherit the polluted values:

if (user.isAdmin) { /* grants administrative access */ }

With the prototype polluted, user.isAdmin returns true even though user never had that property. The result is a privilege escalation that shares its conceptual root with the access control flaws described in What is IDOR: broken access control.

Denial of service (DoS)

Polluting properties that the engine or the framework reads internally can break the whole application. Injecting a value into Object.prototype.toString, or overloading a framework's route handling, causes unhandled exceptions on subsequent legitimate requests. CVE-2022-24999, in the qs package used by Express, allowed exactly that: degrading the service by manipulating the prototype from the query string.

Remote code execution (RCE)

The most severe scenario arrives when a gadget turns an inherited property into execution. The research "Silent Spring: Prototype Pollution Leads to Remote Code Execution in Node.js" (Shcherbakov et al., USENIX Security 2023) documented chains where polluting child_process options (for example shell, env or NODE_OPTIONS with --require) forces Node.js to load and run attacker code. The best known public case is CVE-2019-7609 in Kibana, where prototype pollution in the Timelion component chained all the way to server side RCE.

Prototype pollution on client and server

In the browser, prototype pollution is rarely the end goal: it acts as a stepping stone toward DOM XSS. Many client libraries build elements or URLs from properties that, if polluted, become injection gadgets. Burp Suite's DOM Invader extension automates the detection of these client side gadgets.

On the server (server side prototype pollution, or SSPP), the impact is greater because the gadgets available in Node.js include process execution, module loading and template handling. An exploitable SSPP usually means RCE, not just interface manipulation. This class of flaw fits squarely into the work described in API penetration testing for REST and GraphQL, where JSON bodies are the natural entry surface.

CVEs and real cases

Prototype pollution has a long track record across the npm ecosystem. Some representative examples: lodash (CVE-2019-10744, in defaultsDeep), jQuery (CVE-2019-11358, in $.extend, fixed in 3.4.0), the ini parser (CVE-2020-7788), tough-cookie (CVE-2023-26136) and the already cited Kibana (CVE-2019-7609). The common pattern is always the same: a widely used utility that copies user controlled keys without blocking __proto__ or constructor. Because of its transitive nature, a single vulnerable dependency in the node_modules tree is enough to expose the whole application, something that connects to the risks of the software supply chain.

Defense and hardening

At the code level

The most effective measure is not to use plain objects as dictionaries for untrusted data. Object.create(null) creates prototype less objects, immune to inheritance based pollution, and Map avoids the problem entirely for dynamic key value pairs. When a merge is unavoidable, dangerous keys must be explicitly rejected:

const BLOCKED = ['__proto__', 'constructor', 'prototype'];
for (const key in source) {
  if (BLOCKED.includes(key)) continue;
  // ... safe copy
}

Freezing the prototype at process startup, with Object.freeze(Object.prototype), prevents any later write onto it, although it should be tested because some legacy libraries write to the prototype legitimately.

At the Node.js platform level

Node.js offers the --disable-proto=throw (or --disable-proto=delete) flag that neutralises the __proto__ accessor, closing the most common vector. The experimental --frozen-intrinsics flag goes further and freezes the engine's internal objects. Both should be validated in a staging environment before production.

At the library and dependency level

Validating all input against a strict schema with Ajv and additionalProperties: false discards unexpected keys before they reach any merge. To parse untrusted JSON, secure-json-parse strips dangerous keys. And keeping dependencies up to date (lodash 4.17.21 or later, jQuery 3.4.0 or later) through npm audit, Snyk or Socket closes the known CVEs.

How to detect it in an audit

Detection combines static, dynamic and manual analysis. CodeQL's js/prototype-pollution query and Semgrep rules identify insecure recursive merges in source code. On the dynamic side, DOM Invader (Burp Suite) covers the client, and tools such as proto-find, ppfuzz or ppmap help fuzz parameters on the server. The PortSwigger Web Security Academy labs on prototype pollution are the practical reference for reproducing client and server chains in a controlled environment.

Manual confirmation remains essential: sending {"__proto__":{"secraTest":"x"}} to an endpoint and then checking, in a separate request, whether an unrelated object inherits secraTest is the direct proof that an exploitable sink exists. From there, the work consists of locating the gadget that turns that pollution into real impact, a process analogous to that of other injection classes such as SQL injection or OS command injection.

Frequently asked questions

Does prototype pollution only affect Node.js?

No. It is a JavaScript language problem, so it affects both the backend (Node.js, Deno, Bun) and the browser. On the server it usually leads to RCE or DoS, while on the client it acts as a gadget preceding a DOM XSS. The vector changes, but the root cause (prototypal inheritance and insecure sinks) is the same.

Is blocking the proto key enough?

Not on its own. A filter that only rejects __proto__ leaves the constructor.prototype vector open, which reaches the same shared prototype by another route. The block list must include __proto__, constructor and prototype, and the most robust approach is to combine it with Object.create(null), schema validation and the --disable-proto flag.

Does JSON.parse pollute the prototype by itself?

No. JSON.parse('{"__proto__":{}}') creates __proto__ as an own property of the resulting object and does not touch the global prototype. Pollution happens in a later step, when a merge, a deep clone or a bracket notation assignment copies that key onto Object.prototype. The risk is in the sink, not in the parsing.

How does prototype pollution reach RCE?

It needs a gadget: a piece of code that reads an inherited property and uses it to execute. In Node.js the usual gadgets live in child_process (the shell, env, NODE_OPTIONS with --require options) and in template engines. The Silent Spring research (USENIX 2023) systematised these chains and Kibana (CVE-2019-7609) is the public reference case.

Which libraries have been affected historically?

Among the best known are lodash (CVE-2019-10744), jQuery (CVE-2019-11358), the ini parser (CVE-2020-7788), tough-cookie (CVE-2023-26136) and qs in Express (CVE-2022-24999). Because these utilities are widely used transitive dependencies, a single vulnerable version in the dependency tree can expose the whole application.

Auditing prototype pollution with Secra

Secra performs web application and API audits aligned with OWASP, including a specific hunt for prototype pollution on both client and server. We analyse recursive merges, input parsers, deep clones and the npm dependency tree to locate both the sink and the gadget that turns it into RCE, DoS or authorization bypass.

We deliver prioritised findings, reproducible proofs of concept and concrete hardening recommendations for your Node.js stack: prototype less objects, Ajv validation, --disable-proto flags and updates for vulnerable dependencies. You can review the scope through our web and mobile audit service or write to us via /en/contact/ to plan a review adapted to your platform.

About the author

Secra Solutions team

Ethical hackers with OSCP, OSEP, OSWE, CRTO, CRTL and CARTE certifications, 7+ years of experience in offensive cybersecurity, and authors of CVE-2025-40652 and CVE-2023-3512.

Share article