Prototype Pollution
Prototype Pollution at a glance
Overview
In JavaScript, plain objects inherit from Object.prototype. If untrusted input is merged into objects without filtering, attackers can set special keys like __proto__, constructor, or prototype. Libraries that perform deep merge or extend operations can propagate these keys into the global prototype chain. Once polluted, every new object can appear to have attacker-chosen properties, impacting control flow, logging, serialization, and even rendering in the browser.
Where it occurs
It occurs when applications merge untrusted input into configuration or objects—such as request bodies, CLI arguments, or URL parameters—using deep merge utilities without proper validation.
Impact
Pollution can flip boolean checks such as if (obj.isAdmin), inject unexpected headers, crash processes by overriding toString, or cause persistent XSS in client apps. In multi-tenant servers, process-wide effects increase the blast radius.
Prevention
Prevent prototype pollution by avoiding deep merges of untrusted input, whitelisting keys, stripping reserved properties, normalizing via JSON, restricting merges to plain objects, and enforcing schema-validated, strongly typed data.
Examples
Switch tabs to view language/framework variants.
Merging request body into defaults allows `__proto__` pollution
A handler merges untrusted JSON into a config object. An attacker sets `__proto__` to inject properties into all plain objects.
const merge = require('lodash.merge');
app.post('/search', (req,res)=>{
const cfg = merge({limit:10, flags:{exact:false}}, req.body); // BUG
const q = {term: req.body.term || ''};
// later code creates plain objects that inherit polluted props
res.json({ok:true, cfg});
});- Line 2: Deep merge of untrusted object without prototype guards
lodash.merge composes object graphs. If you pass untrusted objects with special keys like __proto__, they can poison the prototype of all future objects.
const merge = require('lodash.merge');
function safeMerge(dst, src){
if (src && typeof src === 'object') {
if (Object.prototype.hasOwnProperty.call(src,'__proto__') || Object.prototype.hasOwnProperty.call(src,'constructor')) return dst;
}
return merge(dst, src);
}
app.post('/search', (req,res)=>{
const input = JSON.parse(JSON.stringify(req.body)); // strips prototypes
const cfg = safeMerge({limit:10, flags:{exact:false}}, input);
res.json({ok:true, cfg});
});- Line 9: Sanitize input to plain JSON and refuse `__proto__` or `constructor` keys before merge
Treat untrusted input as data only, serialize-deserialize to drop prototypes, and block special keys before deep merge.
Engineer Checklist
-
Block
__proto__,constructor, andprototypekeys at all depths -
Use
Object.create(null)for header maps and dictionaries -
Avoid deep-merge of user input; copy allows-listed keys into fresh objects
-
Normalize and validate request bodies with a schema before merging
-
Audit logging, serialization, and feature flag code paths for reliance on inherited properties
End-to-End Example
An API merges user-provided options into a global config with `lodash.merge`. An attacker sends a body that sets `__proto__.isAdmin = true`. Later, a permission check that creates `{}` and tests `.isAdmin` unexpectedly passes.
// Node.js/Express + lodash - Vulnerable prototype pollution
const _ = require('lodash');
const appConfig = {
theme: 'light',
language: 'en',
features: {}
};
app.post('/api/settings', authenticateToken, (req, res) => {
// VULNERABLE: Deep merge of untrusted user input
// Attacker sends: { "__proto__": { "isAdmin": true } }
// This pollutes Object.prototype for the entire application!
_.merge(appConfig, req.body);
res.json({ message: 'Settings updated', config: appConfig });
});
app.get('/api/admin/users', authenticateToken, (req, res) => {
// VULNERABLE: Authorization check using polluted prototype
const user = {};
user.id = req.user.id;
user.name = req.user.name;
// After pollution, ALL objects inherit isAdmin: true!
if (user.isAdmin) { // This is now true for everyone!
return res.json(getAllUsers());
}
res.status(403).json({ error: 'Admin only' });
});
// ALSO VULNERABLE: Object.assign with nested objects
app.patch('/api/preferences', (req, res) => {
const userPrefs = { notifications: true };
// VULNERABLE: Object.assign doesn't deep clone, but can still be exploited
// with constructor.prototype pollution
Object.assign(userPrefs, req.body);
res.json(userPrefs);
});// Node.js/Express - SECURE against prototype pollution
// Utility to filter dangerous keys recursively
function sanitizeObject(obj) {
const dangerousKeys = ['__proto__', 'constructor', 'prototype'];
if (typeof obj !== 'object' || obj === null) return obj;
if (Array.isArray(obj)) {
return obj.map(sanitizeObject);
}
const sanitized = {};
for (const key in obj) {
if (obj.hasOwnProperty(key) && !dangerousKeys.includes(key)) {
sanitized[key] = sanitizeObject(obj[key]);
}
}
return sanitized;
}
const appConfig = {
theme: 'light',
language: 'en',
features: {}
};
app.post('/api/settings', authenticateToken, (req, res) => {
// SECURE: Whitelist allowed fields, no deep merge
const allowedFields = ['theme', 'language', 'timezone'];
const updates = {};
for (const field of allowedFields) {
if (req.body[field] !== undefined) {
// Coerce to expected type
updates[field] = String(req.body[field]);
}
}
Object.assign(appConfig, updates);
res.json({ message: 'Settings updated', config: appConfig });
});
app.get('/api/admin/users', authenticateToken, (req, res) => {
// SECURE: Explicit permission check, not relying on object properties
const userId = req.user.id;
// Check against database or explicit role system
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Admin only' });
}
res.json(getAllUsers());
});
// SECURE: Use JSON round-trip to remove prototype pollution
app.patch('/api/preferences', (req, res) => {
// JSON round-trip removes __proto__ and other dangerous keys
const sanitizedInput = JSON.parse(JSON.stringify(req.body));
// Additional sanitization
const safeInput = sanitizeObject(sanitizedInput);
// Whitelist specific fields
const userPrefs = {
notifications: Boolean(safeInput.notifications),
email_updates: Boolean(safeInput.email_updates),
theme: String(safeInput.theme || 'light')
};
res.json(userPrefs);
});
// ALTERNATIVE: Use null-prototype objects for maps
app.post('/api/user-data', (req, res) => {
// Create object with no prototype
const userData = Object.create(null);
// Safe to assign - no prototype chain to pollute
userData.name = req.body.name;
userData.email = req.body.email;
res.json(userData);
});Discovery
This vulnerability is discovered by testing object merge operations or query parameter parsing with special keys like __proto__ or constructor.prototype and observing whether properties are added to JavaScript object prototypes.
-
1. Baseline object merge test
httpAction
Send normal nested object to understand merge behavior
Request
POST https://app.example.com/api/settingsHeaders:Content-Type: application/jsonBody:{ "preferences": { "theme": "dark", "language": "en" } }Response
Status: 200Body:{ "message": "Settings updated", "merged_config": { "preferences": { "theme": "dark", "language": "en" } } }Artifacts
merge_operation_confirmed baseline_established -
2. Prototype pollution via __proto__
httpAction
Inject __proto__ key to pollute Object.prototype
Request
POST https://app.example.com/api/settingsHeaders:Content-Type: application/jsonBody:{}Response
Status: 200Body:{ "message": "Settings updated", "test_result": "New empty object {}.polluted === true", "note": "Prototype chain poisoned! All new objects inherit polluted properties" }Artifacts
prototype_pollution_confirmed object_prototype_modified global_state_affected -
3. Constructor.prototype pollution
httpAction
Test alternate pollution vector via constructor.prototype
Request
POST https://app.example.com/api/configHeaders:Content-Type: application/jsonBody:{ "constructor": { "prototype": { "adminMode": true, "bypassAuth": true } } }Response
Status: 200Body:{ "message": "Config merged", "verification": "({}).adminMode === true", "note": "Alternate pollution vector successful" }Artifacts
constructor_pollution prototype_chain_modified -
4. Denial of service via toString pollution
httpAction
Break core JavaScript operations by polluting toString
Request
POST https://app.example.com/api/settingsHeaders:Content-Type: application/jsonBody:{}Response
Status: 500Body:{ "error": "TypeError: Cannot convert object to primitive value", "stack_trace": "at Object.toString...\nat JSON.stringify...\nat logger.info...", "note": "Application crashes on any string coercion operations" }Artifacts
application_crash dos_confirmed serialization_failure
Exploit steps
An attacker exploits this by polluting object prototypes with malicious properties that affect application logic globally, potentially achieving denial of service, authentication bypass, or remote code execution depending on how the polluted properties are used.
-
1. Privilege escalation via isAdmin pollution
Pollute prototype with admin flag
httpAction
Inject isAdmin=true into Object.prototype to bypass authorization
Request
POST https://app.example.com/api/searchHeaders:Content-Type: application/jsonBody:{}Response
Status: 200Body:{ "message": "Search completed", "auth_check_result": "Authorization check: ({}).isAdmin === true", "admin_access": "Granted", "note": "All subsequent requests see isAdmin=true on empty objects, bypassing authorization" }Artifacts
authorization_bypass admin_access_granted privilege_escalation process_wide_pollution -
2. Remote code execution via template pollution
Pollute template engine properties for RCE
httpAction
Inject properties that affect template rendering to execute code
Request
POST https://app.example.com/api/renderHeaders:Content-Type: application/jsonBody:{}Response
Status: 200Body:{ "rendered": "DATABASE_URL=postgresql://admin:Pr0dP@ssw0rd2024@db.internal:5432/production\nAWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE\nAWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "note": "Template engine executed attacker code using polluted properties" }Artifacts
rce_confirmed secrets_extracted template_pollution_exploit code_execution -
3. Application-wide denial of service
Crash all request handling
httpAction
Pollute critical methods to break JSON serialization and logging
Request
POST https://app.example.com/api/configHeaders:Content-Type: application/jsonBody:{}Response
Status: 500Body:{ "error": "Service Unavailable", "impact": "All subsequent requests fail with TypeError", "affected_operations": [ "logging", "JSON.stringify", "string concatenation", "HTTP responses" ], "duration": "Until process restart", "note": "Process-wide DoS affecting all users" }Artifacts
application_down process_wide_dos service_disruption multi_tenant_impact -
4. Data exfiltration via polluted callbacks
Inject malicious error handlers
httpAction
Pollute callback URLs to intercept sensitive error data
Request
POST https://app.example.com/api/webhookHeaders:Content-Type: application/jsonBody:{}Response
Status: 200Body:{ "message": "Webhook configured", "attacker_receives": { "error_logs": "Database connection errors with credentials", "stack_traces": "Full application source paths and logic", "user_data": "PII from failed operations", "note": "All errors and logs now sent to attacker endpoints" } }Artifacts
data_exfiltration error_log_theft credential_leak source_disclosure
Specific Impact
Authorization checks that rely on object properties fail in unpredictable ways. Attackers may enable admin features or disable safeguards. In some cases, polluted toString breaks logging and incident monitoring, causing availability issues and delayed detection.
Because pollution is process-wide, multiple tenants or requests can be affected until the process restarts, increasing blast radius and remediation time.
Fix
Replace deep-merge of user input with allow-listed copying into null-prototype objects. Strip reserved keys recursively and normalize inputs with JSON serialization. Add unit tests that attempt to set __proto__ and confirm rejection.
Detect This Vulnerability in Your Code
Sourcery automatically identifies prototype pollution vulnerabilities and many other security issues in your codebase.
Scan Your Code for Free