Last updated: April 26, 2026
Prototype pollution is a JavaScript-specific bug where attackers modify the Object.prototype, affecting every object in the application. Originally a curiosity, prototype pollution gained mainstream attention with multiple high-profile CVEs in popular npm packages (lodash, jQuery, and many more). This article covers detection, exploitation, and the modern defences.
The bug
// Vulnerable merge function
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
merge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
}
// Attacker payload:
const evil = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, evil);
// Now every object has isAdmin = true, including objects elsewhere in the app
({}).isAdmin // true
The __proto__ key isn’t a regular property — it’s the chain to Object.prototype. Setting it modifies the global prototype, affecting every object created from then on.
Common sinks
lodash.merge,lodash.set,lodash.defaultsDeep(all had CVEs)jQuery.extend(true, ...)(deep copy)- Custom merge / setNested / cloneDeep functions
- Express body-parser configurations that allow raw JSON without validation
- YAML / TOML / config-loaders that allow merging
Detection
# Static analysis
semgrep --config "p/javascript" --include "*.js" --include "*.ts" .
# Look for vulnerable patterns
grep -rE "lodash\.(merge|set|defaultsDeep)|\\\$\\.extend\\(true|JSON\\.parse\\(.*req\\." src/
# Test endpoints by sending payloads with __proto__ keys
curl -X POST https://target/api/config \
-H "Content-Type: application/json" \
-d '{"__proto__": {"polluted": "yes"}}'
# Then test if pollution worked:
curl https://target/api/health
# Response with extra "polluted": "yes" field = vulnerable
Exploitation paths
- Authentication bypass — pollute
isAdmin,role,permissionsdefault values - RCE in Node.js — pollute prototype properties used by template engines or child_process spawn (chained vulnerabilities)
- DoS — pollute toString or prototype methods used in critical paths
- Logic bypass — pollute defaults that flow into business logic
The fix
- Use Object.create(null) for objects that hold user input — no prototype
- Validate keys — reject keys named
__proto__,prototype,constructor - Use Map / Set instead of plain objects where possible
- Update vulnerable dependencies — lodash 4.17.21+, jQuery 3.5.0+
- Object.freeze(Object.prototype) at app startup — prevents pollution at runtime; some libraries break with this
// Safe merge example
function safeMerge(target, source) {
for (const key of Object.keys(source)) {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
if (typeof source[key] === 'object' && source[key] !== null) {
target[key] = target[key] || Object.create(null);
safeMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
Compliance angle
- OWASP Top 10 A03 (Injection) — prototype pollution falls under broader injection class
- DPDP §8(5) — exploitation leading to authentication bypass is reasonable-security failure
The takeaway
Prototype pollution is JavaScript’s quiet bug class. Static analysis catches obvious cases; manual review catches subtle ones. The fix is library updates + key validation + Object.create(null) for user-input-holding objects. Audit your Node.js dependencies for known-vulnerable versions of merge utilities.
Get a VAPT scoping call
Senior practitioner-led VAPT — not a checklist run by juniors. CVSS-scored findings, free retest, attestation letter. India's SMBs and SaaS teams.