Advanced levelFree

Prototype Pollution — from malicious JSON to XSS, RCE and auth bypass

How an attacker pollutes Object.prototype and breaks the code's assumptions. Client-side and server-side vectors, gadgets in lodash/jQuery/Angular.

Gorka El BochiMay 9, 202613 min

Quick answer

Prototype Pollution lets an attacker modify Object.prototype (or Array.prototype, etc.) in JavaScript, polluting ALL objects in the runtime. Gadgets in libraries (lodash merge, jQuery extend) make it easy. Consequences: client-side XSS via UI-library gadgets, server-side RCE (Node.js execSync with argv override), bypassing auth checks that trust object properties.


How it works

In JavaScript, every object inherits from Object.prototype. If you do:

javascript
Object.prototype.isAdmin = true;
const user = {};
console.log(user.isAdmin);  // true — inherited from prototype

If user input reaches obj[key] = value with key === "__proto__":

javascript
const obj = {};
obj["__proto__"]["isAdmin"] = true;  // pollutes Object.prototype

Any {} created afterwards will have isAdmin = true by default.


Typical vector — JSON merge

javascript
function merge(target, source) {
  for (const key in source) {
    if (typeof source[key] === 'object') {
      if (!target[key]) target[key] = {};
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
}

// Attacker sends
const malicious = JSON.parse('{"__proto__":{"isAdmin":true}}');
merge({}, malicious);

// Now any object has isAdmin=true
console.log({}.isAdmin);  // true

Lodash _.merge and _.set had historical CVEs for this pattern. Patched in recent versions, but apps with old lodash or custom merge remain vulnerable.


Server-side (Node.js)

Vector 1: Express body-parser + recursive merge

javascript
const _ = require('lodash');
const cfg = require('./config');

app.post('/settings', (req, res) => {
  _.merge(cfg, req.body);  // ⚠️ user input merged into a global config
  res.send('ok');
});

// Attacker sends JSON
{ "__proto__": { "polluted": "value" } }

After the pollution, any library in the runtime that has a code path like:

javascript
function callShell(cmd, args) {
  const opts = { shell: '/bin/bash' };
  if (config.shell) opts.shell = config.shell;
  return execSync(cmd, opts);
}

If config.shell is not explicitly defined and the attacker pollutes Object.prototype.shell = "node -e 'require(\"child_process\").execSync(...)' ", RCE.

Vector 2: argv override in child_process

Real case (CVE-2019-7609 Kibana):

javascript
// Attacker pollutes Object.prototype.NODE_OPTIONS
{ "__proto__": { "NODE_OPTIONS": "--inspect-brk=0.0.0.0:9229" } }

When some module invokes child_process.spawn('node', ['some.js']), Node.js reads NODE_OPTIONS from the environment. If the lib passes env as an object and the merge polluted __proto__.NODE_OPTIONS, the child node opens a remote debugger → connection + RCE via debugger.

Vector 3: Rendering engine (Pug/Handlebars)

Some templates use Object.assign(defaults, userOptions). If defaults isn't created with Object.create(null), it pollutes globals:

javascript
const opts = Object.assign({}, userOpts);  // userOpts may have __proto__
opts[malicious_key]  // accesses the polluted prototype

Client-side prototype pollution (CSPP)

Vector: query string or hash that the frontend parses with a vulnerable parser:

javascript
// jQuery extend (vulnerable < 3.4.0)
$.extend(true, options, parsed_hash);

// URL: https://target.tld/#/?__proto__[isAdmin]=true
// After parsing, Object.prototype.isAdmin = true

Gadgets in client-side:

  • AdSense / Analytics that checks document properties.
  • UI library that has if (config.template) document.write(config.template).
  • Form builder that renders inputs based on field.html.

Once Object.prototype.html is polluted, any field.html (where field = {}) is worth the attacker's value. If the lib does innerHTML = field.html, persistent XSS on the client.


Detection

Burp DOM Invader (CSPP scanner)

Built-in in Burp Pro. Injects payloads into location.hash, location.search, body. Detects gadgets that use the polluted values.

Manual

javascript
// Inject and check if Object.prototype changes
location.hash = '#/?__proto__[test]=polluted';
// After the lib parses it:
console.log(({}).test);  // if "polluted", it pollutes globals

Server-side detection

POST to JSON endpoints with {"__proto__": {"canary_polluted": "yes"}}. Do a subsequent GET and check whether the app responds with the canary somewhere (serialized object, error message, etc.).


List of useful gadgets

In libraries

  • lodash _.merge, _.set, _.defaultsDeep (pre-4.17.12 vuln).
  • jQuery $.extend(true, ...) (pre-3.4.0).
  • express req.body with qs extended deep (pre-1.13).
  • mongoose schema merge.
  • mootools class init.

Vulnerable code patterns

javascript
// 1. Recursive merge without __proto__ filter
function deepMerge(t, s) {
  for (const k in s) {
    if (typeof s[k] === 'object') deepMerge(t[k] || (t[k] = {}), s[k]);
    else t[k] = s[k];
  }
}

// 2. Path-based set
function set(obj, path, value) {
  const keys = path.split('.');
  let curr = obj;
  for (let i = 0; i < keys.length - 1; i++) {
    if (!curr[keys[i]]) curr[keys[i]] = {};
    curr = curr[keys[i]];
  }
  curr[keys[keys.length - 1]] = value;
}

// 3. Object.assign to globals (rare but it exists)
Object.assign(Object.prototype, userInput);  // ⚠️ double vulnerability

Correct mitigation

  1. Object.create(null) for objects that receive user input. They have no prototype, so __proto__ pollutes nothing.
  2. Filter __proto__, constructor.prototype, prototype in merge functions.
  3. Object.freeze(Object.prototype) at process boot. Any pollution attempt fails.
  4. JSON.parse reviver that rejects dangerous keys.
  5. TypeScript with strict types avoids many cases by design.
  6. Lodash 4.17.12+, jQuery 3.4.0+, etc.

Hunting checklist

  • Does the app use lodash, jQuery, mootools, or old frameworks?
  • Are there endpoints that accept JSON deeply merged into state?
  • Does the frontend parse location.hash or location.search with jQuery extend?
  • Try {"__proto__":{"canary":"polluted"}} and check whether it propagates?
  • Does Burp DOM Invader detect gadgets?
  • After pollution, are there code paths that use properties without verifying (config.shell, options.html)?

Practice client + server side prototype pollution and gadget chains toward XSS/RCE: Prototype Pollution labs.

Practice this in a lab

Prototype Pollution

Solve

Keep learning · free account

Save your progress, unlock advanced payloads and rank your flags.

Create account

Related articles

hunters training
711

hunters training

labs from real reports
55

labs from real reports

completions
1,205

completions

in bounties practiced
$213,970

in bounties practiced

46 flags captured this week·Real reports from HackerOne · Bugcrowd · Intigriti·No commitment·Free Academy