DOM XSS in document.write sink using source location.search

Marduk I Am
5 min readDec 17, 2023

This is the third lab in a series from PortSwigger Web Security Academy, focusing on cross-site scripting (XSS).

The description states: This lab contains a DOM-based cross-site scripting vulnerability in the search query tracking functionality. It uses the JavaScript document.write function, which writes data out to the page. The document.write function is called with data from location.search, which you can control using the website URL. To solve this lab, perform a cross-site scripting attack that calls the alert function.

This lab is a little more involved than the reflected and stored XSS labs. I will be taking more of a “scenic route” in solving this lab so we really understand what is happening.

A little context before we start. A Document Object Model (DOM) tree is a hierarchical representation of an HTML or XML document. In your browser’s developer tools look for the Inspector tab or your browser’s equivalent. I’m using Firefox, if you are using Chrome look for Element.

Screenshot of the developer tools screen with a red arrow pointing at the Inspector tab.

This is the DOM-browser. It allows you to easily navigate though the workings of a web page. The DOM tree is not JavaScript, however, it can be manipulated by it. Manipulated by scripts and special characters like we used in the reflected and stored XSS labs.

In this lab we are taken to another blog page with a search bar similar to our reflected XSS lab. Might as well check to see if the reflected XSS vulnerability was fixed. I entered <script>alert(document.cookie)</script> into the search bar to see how the page would respond.

Screenshot of search result that displays “0 search results for ‘<script>alert(document.cookie)</script>’” on the page.

Our entire script, including tags, was returned on the web page. Let’s see what is happening in the DOM browser. Right click on your search result and select Inspect from the drop-down.

Screenshot of DOM browser with red boxes highlighting the 2 places where the search result are displayed.

Notice our script appears twice in the DOM browser. Once in the <h1> tag, within single quotes, and once again in the <img src=…> tag. But why? Look above the <img> tag at the contents of the <script> tag.

<script>
function trackSearch(query) {
document.write('<img src="/resources/images/tracker.gif?searchTerms=' + query + '">');
}
var query = (new URLSearchParams(window.location.search)).get('search');
if (query) {
trackSearch(query);
}
</script>

Let’s break this down a little bit.

  • function trackSearch(query) — Defines a function that takes a ‘query’ as a parameter.
  • document.write — creates ‘<img src=”/resources/images/tracker.gif?searchTerms=’ adding whatever the variable ‘query’ is after the = sign.
  • var query — creates a variable named ‘query’. It will .get this from the ‘search’ property in the current query string ‘window.location.search’ using the ‘URLSearchParams’ object.
  • ‘window.location’ — Current URL of the browser
  • ‘.search’ — is a property of ‘window.location’ that returns the query string of the URL (the part of the URL after the question mark ?).

This script is designed to track the search queries in the URL. If a ‘search’ parameter is present in the URL, it calls the trackSearch function to generate an image tag with a tracking URL that includes the search query. This is commonly used for analytics or logging user interactions on a website.

Our original reflected XSS script didn’t work. But why not? We need to “break out” of the site’s script so we can inject our own script. The key to “breaking out” of this context will involve manipulating the value of ‘query’ or ‘.search’.

I am going to switch our query to something a little more easy to read, but I will add a double quote to the beginning to see what happens. To start with I search for: “ MardukWasHere

Screenshot of search result that displays “0 search results for ‘“ MardukWasHere’” on the page.
Screenshot of the DOM browser with a red box highlighting <img src=”/resources/images/tracker.gif?searchTerms=” MardukWasHere”=””>

Notice the injection of the double quote at the beginning of the search parameter prematurely closes the src attribute value.

We “broke out” of the src attribute! Now if we change mardukwashere to an executable script tag, we may be able to solve this lab.

Going to use the following payload:

"><img src=x onerror=alert('MardukWasHere')>

Here’s the break down:

  • “> — Closes the first src attribute and img element.
  • <img — Opens a new img element.
  • src=x — Sets new image source to a non-existent resource (x), triggering …
  • onerror — Attribute of the img element. When the source fails …
  • alert() — Creates pop-up window
  • alert(‘MardukWasHere’) — Specific message to be displayed within single quotes.

It worked! Congratulations!

Screenshot of the pop-up window we created with our script.
Screenshot of DOM browser with red rectangle highlighting how our script closed one img element and opened another one.

You can see the script injection worked. We were able to break out of one img element and create a new one. Our payload triggered an onerror event, our pop-up window, effectively injecting and executing our own script within the page. This exercise underscored the importance of understanding the inner workings of the Document Object Model (DOM) and how it can be manipulated to exploit vulnerabilities.

Thank you for reading!

Reflected XSS Lab: https://medium.com/@marduk.i.am/reflected-xss-into-html-context-with-nothing-encoded-8d3fb3a9eaf5

Stored XSS Lab: https://medium.com/@marduk.i.am/stored-xss-into-html-context-with-nothing-encoded-e0ba5f2a1952

--

--

Marduk I Am

Cybersecurity enthusiast. Currently focusing on write-ups and bug bounties. Twitter: @marduk_I_am | Mastodon: @Marduk_James@infosec.exchange