PortSwigger — 2026.01.09

Client-Side Prototype Pollution via Browser APIs

PortSwigger Prototype Pollution to DOM XSS (descriptor gadget) medium

Overview

Lab: Client-side prototype pollution via browser APIs 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:

var deparam = function( params, coerce ) {
    var obj = {},
    // ...
    for ( ; i <= keys_last; i++ ) {
        key = keys[i] === '' ? cur.length : keys[i];
        cur = cur[key] = i < keys_last
            ? cur[key] || ( keys[i+1] && isNaN( keys[i+1] ) ? {} : [] )
            : val;
    }
    // ...
};

When key is __proto__, cur[key] returns Object.prototype, and subsequent iterations pollute it.

The Gadget (with Defense)

async function searchLogger() {
    let config = {params: deparam(new URL(location).searchParams.toString()), transport_url: false};
    Object.defineProperty(config, 'transport_url', {configurable: false, writable: false});
    if(config.transport_url) {
        let script = document.createElement('script');
        script.src = config.transport_url;
        document.body.appendChild(script);
    }
    // ...
}

Initial observation: config.transport_url is set to false as an own property, which should shadow any prototype pollution. The defense: Object.defineProperty makes the property non-writable.


The Bypass - Browser API Exploitation

Why Standard Pollution Fails

Polluting Object.prototype.transport_url doesn’t work because:

  1. config has transport_url: false as an own property
  2. Own properties take precedence over prototype properties
  3. if(config.transport_url) returns false

The Key Insight

Look at the Object.defineProperty call:

Object.defineProperty(config, 'transport_url', {configurable: false, writable: false});

The property descriptor object {configurable: false, writable: false} is missing several properties:

  • value
  • enumerable
  • get
  • set

Critical realization: The descriptor object is a plain JavaScript object. If it doesn’t have an own value property, it will inherit from Object.prototype!

The Attack Chain

  1. User visits URL with ?__proto__[value]=data:,alert(document.domain)
  2. deparam parses the query string and pollutes Object.prototype.value
  3. config = {..., transport_url: false} creates object with own property set to false
  4. Object.defineProperty(config, 'transport_url', {configurable: false, writable: false}) executes
  5. The descriptor {configurable: false, writable: false} has no own value property
  6. JavaScript looks up prototype chain -> finds Object.prototype.value = "data:,alert(document.domain)"
  7. Object.defineProperty overwrites config.transport_url with the polluted value
  8. if(config.transport_url) is now truthy
  9. script.src = "data:,alert(document.domain)" -> XSS!

Solution

Payload

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

Why data: URI?

The injection point is <script src="...">. Unlike javascript: URIs (which work for <a href> or <iframe src>), script src attributes require a URL that serves JavaScript content.

The data: URI scheme embeds content directly:

data:[mediatype],content
  • data:,alert(1) - Minimal form (comma separates metadata from content)
  • data:text/javascript,alert(1) - With explicit MIME type

Key Takeaways

  1. Browser APIs can be pollution gadgets: Object.defineProperty reads from a descriptor object that can inherit polluted properties
  2. Defense code can become the vulnerability: The Object.defineProperty call intended as defense actually enables the attack by reading the polluted value property
  3. Own properties aren’t always safe: Even when an own property exists, browser APIs that read from other objects (like descriptors) can still be exploited
  4. Property descriptor pollution: Any code using Object.defineProperty with incomplete descriptors is potentially vulnerable if prototype pollution exists

Remediation

  1. Sanitize prototype pollution sources: Filter __proto__, constructor, and prototype from user input

  2. Use Object.create(null) for descriptors: Creates objects without prototype inheritance
    const descriptor = Object.create(null);
    descriptor.configurable = false;
    descriptor.writable = false;
    Object.defineProperty(config, 'transport_url', descriptor);
    
  3. Explicitly set all descriptor properties: Always include value (or get/set) in property descriptors
    Object.defineProperty(config, 'transport_url', {
     value: false,  // Explicit!
     configurable: false,
     writable: false
    });
    
  4. Freeze Object.prototype: Prevents pollution but may break third-party code
    Object.freeze(Object.prototype);
    
#prototype-pollution #dom-xss #define-property #descriptor #portswigger #webapp