Skip to content
Field Detail
Platform PortSwigger Web Security Academy
Type Prototype Pollution / DOM XSS
Difficulty Practitioner
Objective Pollute Object.prototype via the URL query string to inject transport_url into the page's config object and trigger alert()

DOM XSS via Client-Side Prototype Pollution

Testing the URL query string for prototype pollution:

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

Pollution confirmed. Browsing to searchLogger.js in DevTools:

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

config is created with only a params property — transport_url is never defined as an own property on the object. When config.transport_url is evaluated, the JavaScript engine falls through to the prototype chain, which we control. This is the unpatched version of the gadget from the previous lab: no Object.defineProperty() attempting to lock anything down. The gadget works precisely because transport_url is left undefined on config — own properties always shadow prototype properties. If the developer had written transport_url: false, prototype pollution couldn't override it.

Injecting directly:

/?__proto__[transport_url]=data:,alert(1)

Object.prototype.transport_url is set, searchLogger() fires on page load, config.transport_url resolves through the prototype chain to data:,alert(1), if(config.transport_url) is truthy, and <script src="data:,alert(1)"> gets appended to the DOM.

Screenshot

When the source is the URL and the gadget flows directly to script.src, the entire exploit fits in a single crafted URL. No interaction, no server, no chaining — just navigate and the payload executes.

Lab solved :P

Resources