Skip to content
Field Detail
Platform PortSwigger Web Security Academy
Type Prototype Pollution / DOM XSS
Difficulty Practitioner
Objective Find a prototype pollution source and a gadget to call alert() by bypassing a developer's attempted patch on transport_url

Client-Side Prototype Pollution via Browser APIs

I tested the URL query string as a prototype pollution source — the most common entry point — by injecting a test property via __proto__:

/?__proto__[tetoProperty]=miku
Screenshot
console.log({}.tetoProperty)
// → miku

Prototype pollution confirmed. Any property injected into __proto__ is now inherited by all objects in the page's JavaScript runtime.

Browsing the source in DevTools led to searchLoggerConfigurable.js:

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

transport_url controls a script.src — a classic gadget. The developer patched it with Object.defineProperty() setting configurable: false, writable: false. This looks locked down, but there's a gap.

Object.defineProperty() accepts a property descriptor — which is itself a plain JavaScript object inheriting from Object.prototype. The developer specified configurable: false and writable: false but didn't specify value. Without a value field, the property is defined with value: undefined, not the previous false. More importantly, if Object.prototype.value is polluted, that value bleeds into the descriptor when defineProperty reads it — causing the property to be set to our payload. The developer locked the property but left the descriptor object open to prototype contamination.

The execution chain: Object.prototype.value = 'data:,alert(1);'defineProperty picks it up from the prototype → config.transport_url gets set to our payload → if(config.transport_url) is truthy → <script src="data:,alert(1);"> gets appended → alert(1) fires.

Injecting via the URL:

/?__proto__[value]=data:,alert(1);
Screenshot

Alert fired. data:,alert(1) is a valid script source — the browser executes content after the comma as JavaScript, no external server needed.

Lab solved

Resources