In JavaScript, every object inherits from
Object.prototype. If an attacker can modify that prototype, they can inject properties into every object in your application — including ones that control authentication.
What Prototype Pollution Is
JavaScript objects inherit properties from their prototype chain. Object.prototype sits at the top of this chain for most objects. If you can write to it, you affect every object:
// Polluting the prototype
const obj = {};
obj.__proto__.isAdmin = true;
// Now EVERY object has isAdmin = true
const user = {};
console.log(user.isAdmin); // trueThis is not a theoretical concern. It is a real vulnerability class with CVEs in widely-used libraries.
How It Happens in Practice
Prototype pollution occurs when user-controlled input is used in a recursive merge, deep clone, or property assignment without checking for dangerous keys.
The Vulnerable Pattern
// A naive deep merge function
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];
}
}
return target;
}
// Attacker-controlled input
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
const config = {};
merge(config, malicious);
// Every object is now polluted
const user = {};
console.log(user.isAdmin); // true — authentication bypassedThe attacker sends {"__proto__": {"isAdmin": true}} as JSON. The merge function walks into __proto__ and writes to Object.prototype, polluting all objects.
Exploiting for Authentication Bypass
// Server-side code
function checkAccess(user) {
if (user.role === "admin") {
return true; // grant access
}
return false;
}
// Before pollution
const normalUser = { name: "Alice" };
checkAccess(normalUser); // false
// After pollution via __proto__.role = "admin"
checkAccess(normalUser); // true — normalUser.role resolves to "admin" via prototypeExploiting for RCE (Remote Code Execution)
In server-side JavaScript, prototype pollution can lead to code execution through template engines or child process spawning:
// If Object.prototype is polluted with:
// { "shell": "/proc/self/exe", "env": { "NODE_OPTIONS": "--require /tmp/malicious.js" } }
// Then any child_process.spawn() call inherits these options
const { execSync } = require("child_process");
execSync("ls"); // executes with polluted env — runs attacker's codeReal-World CVEs
Lodash (CVE-2020-8203)
lodash.merge, lodash.defaultsDeep, and lodash.set were all vulnerable to prototype pollution. Lodash has over 25 million weekly npm downloads.
const _ = require("lodash");
// This polluted Object.prototype in lodash < 4.17.16
_.merge({}, JSON.parse('{"__proto__": {"polluted": true}}'));
console.log({}.polluted); // truejQuery (CVE-2019-11358)
jQuery's $.extend was vulnerable when performing deep merges:
// jQuery < 3.4.0
$.extend(true, {}, JSON.parse('{"__proto__": {"polluted": true}}'));
console.log({}.polluted); // trueminimist (CVE-2020-7598)
The popular argument parser was vulnerable:
# This polluted Object.prototype
node app.js --__proto__.polluted=trueDefenses
1. Block dangerous keys
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;
}2. Use Object.create(null)
Objects created with Object.create(null) have no prototype — they cannot be polluted:
// Normal object — has prototype
const obj = {};
console.log(obj.toString); // [Function: toString]
// Null-prototype object — no prototype chain
const safe = Object.create(null);
console.log(safe.toString); // undefined
// Cannot be affected by prototype pollutionUse this for configuration objects, lookup maps, and any object that stores user-influenced data.
3. Use Map instead of plain objects
// ❌ Plain object — vulnerable to prototype pollution
const cache = {};
cache[userInput] = value;
// ✅ Map — not affected by prototype pollution
const cache = new Map();
cache.set(userInput, value);4. Freeze the prototype
// Prevent any modification to Object.prototype
Object.freeze(Object.prototype);
// Now pollution attempts throw in strict mode or silently fail
"use strict";
({}).__proto__.isAdmin = true; // TypeError: Cannot add property isAdminThis is aggressive and may break some libraries, but it is effective for applications you fully control.
5. Validate input with schemas
// Using zod to validate input — rejects unexpected keys
import { z } from "zod";
const UserSchema = z.object({
name: z.string(),
email: z.string().email(),
});
// Strips __proto__ and any other unexpected keys
const safeInput = UserSchema.parse(untrustedInput);6. Use hasOwnProperty checks
// ❌ Checking property existence without hasOwnProperty
if (user.isAdmin) {
// Vulnerable — isAdmin could come from polluted prototype
}
// ✅ Check own properties only
if (Object.hasOwn(user, "isAdmin") && user.isAdmin) {
// Safe — only checks properties directly on the object
}Detection
Scan your dependencies for known prototype pollution vulnerabilities:
npm audit
# Look for "Prototype Pollution" in the output
# Or use Snyk
npx snyk testConclusion
Prototype pollution is subtle and dangerous. It turns a simple property merge into authentication bypass or remote code execution. Block __proto__, constructor, and prototype keys in all merge operations. Use Object.create(null) or Map for data stores. Validate all input with schemas. And keep your dependencies updated — lodash, jQuery, and minimist all had prototype pollution CVEs that affected millions of applications.
Related posts
Content Security Policy: A Practical CSP Implementation Guide for Developers
Learn how to implement Content Security Policy headers correctly, avoid common misconfigurations, and stop XSS attacks before they reach your users.
Mar 30, 2026 · 9 min readCORS Explained: The 5 Most Dangerous Misconfigurations and How to Fix Them
Understand how CORS works, why browsers enforce it, and the five most common misconfigurations that expose your API to cross-origin attacks.
Mar 29, 2026 · 7 min read