PortSwigger — 2026.01.09

DOM XSS via Client-Side Prototype Pollution

PortSwigger Prototype Pollution to DOM XSS medium

Overview

Lab: DOM XSS via client-side prototype pollution Platform: PortSwigger Web Security Academy Vulnerability: Client-side Prototype Pollution -> DOM XSS Difficulty: Practitioner


Vulnerability Analysis

The Pollution Source

The application uses a vulnerable deparam function to parse URL query parameters. This function is vulnerable to prototype pollution via bracket notation:

?__proto__[property]=value

The Gadget

async function searchLogger() {
    let config = {params: deparam(new URL(location).searchParams.toString())};

    if(config.transport_url) {
        let script = document.createElement('script');
        script.src = config.transport_url;
        document.body.appendChild(script);
    }

    if(config.params && config.params.search) {
        await logQuery('/logger', config.params);
    }
}

Key Observation

The config object is created with only one own property: params

let config = {params: deparam(...)};

The code then checks config.transport_url, but this property does not exist as an own property on config.


The Attack

Why This Works (Direct Pollution)

Unlike more complex labs with defenses, this one has no protection:

  1. config has no own transport_url property
  2. No Object.defineProperty calls
  3. No explicit transport_url: false initialization

When JavaScript evaluates if(config.transport_url):

  1. Checks config for own property transport_url -> not found
  2. Looks up prototype chain -> Object.prototype.transport_url
  3. If polluted, returns attacker’s value -> truthy!
  4. Script injection executes

Attack Chain

  1. User visits URL with ?__proto__[transport_url]=data:,alert(document.domain)
  2. deparam parses query string and pollutes Object.prototype.transport_url
  3. config is created with only params property
  4. if(config.transport_url) checks for property
  5. No own property found -> inherits from polluted Object.prototype
  6. Condition is truthy -> script element created
  7. script.src = "data:,alert(document.domain)" -> XSS!

Solution

Payload

?__proto__[transport_url]=data:,alert(document.domain)

Why Bracket Notation Works (But Dot Notation Doesn’t)

The deparam function specifically parses bracket notation for nested objects:

if ( /\[/.test( keys[0] ) && /\]$/.test( keys[ keys_last ] ) ) {
    keys[ keys_last ] = keys[ keys_last ].replace( /\]$/, '' );
    keys = keys.shift().split('[').concat( keys );
    keys_last = keys.length - 1;
}

With brackets __proto__[transport_url]=evil:

  • Regex detects [ -> splits into ['__proto__', 'transport_url']
  • Traverses: obj['__proto__']['transport_url'] = 'evil'
  • obj['__proto__'] returns Object.prototype -> pollution!

With dot notation __proto__.transport_url=evil:

  • No [ detected -> no nesting
  • Creates literal property: obj['__proto__.transport_url'] = 'evil'
  • Just a weird property name -> no pollution

Comparison with “Browser APIs” Lab

Aspect This Lab (Direct) Browser APIs Lab
Own property transport_url No Yes (false)
Object.defineProperty No Yes
Pollution target transport_url directly value (descriptor hijack)
Complexity Simple Requires bypass

Key Takeaways

  1. Missing own properties are vulnerable: If code checks a property that doesn’t exist on the object, prototype pollution can supply the value
  2. Parser syntax matters: deparam uses bracket notation [] for nesting, not dot notation .
  3. Always check object initialization: Look for what properties are explicitly defined vs. assumed

Remediation

  1. Initialize all expected properties:
    let config = {
     params: deparam(...),
     transport_url: null  // Explicit initialization
    };
    
  2. Use hasOwnProperty checks:
    if(config.hasOwnProperty('transport_url') && config.transport_url) {
     // ...
    }
    
  3. Sanitize pollution sources: Filter __proto__, constructor, prototype from user input

  4. Use Object.create(null): Creates objects without prototype inheritance
    let config = Object.create(null);
    config.params = deparam(...);
    
#prototype-pollution #dom-xss #deparam #gadget #portswigger #webapp