Understanding DOM-Based XSS: Sources and Sinks

FATH3AD
7 min readMay 11, 2022

--

This post assumes the reader has some knowledge on HTML, JavaScript (JS), and Cross-site scripting (XSS). I still try to explain some of these concepts in a way that would be simple to grasp and understand. For a general understanding of Cross-site scripting, please see the links in the references as well as PortSwigger’s Web Academy section on Cross-Site Scripting.

Understanding the DOM

In order to understand sources and sinks as it relates to DOM-Based Cross-site scripting, we briefly have to understand the DOM and some of its methods. The Document Object Model is the web browser’s hierarchical representation of the elements of a webpage. In other words, when a browser receives a page to load, it will parse or dissect the page structure and separate the different elements of the page into a tree-like structure with each element and attribute nested in their respective location.

Simple HTML code Structure
HTML DOM tree of page elements and attributes

As shown in the example above, the root element “<html>” is nested under the “Document” node, which is the page itself, and all other elements and attributes follow suit. Since HTML pages are static, meaning the structure of the page is presented exactly how it’s stored on the server without modification, JavaScript is usually included to make web pages more interactive. As the browser is parsing the page to develop the DOM tree, when it reaches the part of the code that contains JavaScript, the browser will render the code and produce an output based on the logic. Here’s a simple example where we’ll change the header “My Header” to “JS Header”

Code change to include Script tag
Page output as a result of JavaScript code

In the code above, we can see that we initially set the H1 header to be “My Header” but the JavaScript code we’ve included in the script tag tells the browser to change the text of the header to “JS Header”. Therefore, when the browser is parsing, it will initially render “My Header” but once it gets to the JavaScript code, the results will change, making the page interactive.

Note: The DOM can be viewed by right-clicking on the page and selecting “Inspect Element”. This will show the rendered output based on the JS code that’s loaded. You can view the original source code of the page by right-clicking the page and selecting “view source”.

DOM Elements in the browser’s dev tools

DOM-Based XSS: Source and Sinks

Now that we have the basics of the DOM and we understand that JavaScript code can make changes to the page in real time, we can discuss DOM-Based XSS as well as source and sinks. Disclaimer: To help with the explanation, I’ll be using a lab from PortSwigger’s Web Academy, which include additional information as well as plenty of labs to practice the concepts discussed. This will be a minor spoiler to one of the labs.

DOM-Based XSS

Remember JavaScript code is handled and rendered by the user’s browser, either as the page is being parsed or even as the user is interacting with the page. In some cases, web servers will simply respond with a base HTML page with very little html but will have a bunch of JS code which will then handle the rest of the user’s interactions. This helps lower the overhead usage of the server because interactions are being handled on the client-side (browser).

DOM based cross site scripting occurs when JavaScript code accepts a user’s input (source) and passes that input to another function that displays the results back to the page (sink) in an unsafe manner. Unsafe meaning, there’s no security checks on the input before is displayed back on the screen, more on that later. The key thing to keep in mind here is that DOM based requests/attacks aren’t persisted or handled by the server but on the user’s browser. Therefore, depending on the functions used in the JavaScript code on the loaded page, a DOM-Based XSS vulnerability could be trivial to discover and exploit.

Source

A source function is any JS property or function that accepts user input from somewhere on the page. An example of a source is the location.search property because it reads input from the query string.

Here are some common sources:

  • document.URL
  • document.documentURI
  • document.URLUnencoded
  • document.baseURI
  • location.search
  • document.cookie
  • document.referrer

Sink

A sink is a potentially dangerous JavaScript function that can caused undesirable effects if attacker controlled data is passed to it. Basically, if the function returns input back to the screen as output without security checks, it’s considered a sink. An example of this would be the “innerHTML” property used earlier as that changes the contents of the HTML page to whatever is given to it.

Common sinks include:

  • document.write()
  • document.writeln()
  • document.domain
  • element.innerHTML
  • element.outerHTML
  • element.insertAdjacentHTML
  • element.onevent

DOM-Based XSS Example

In our example, we have a web page that handles the storeId parameter strictly from within the client-side code.

Webpage vulnerable to DOM-Based XSS

Knowing some common sources already, we can search through the scripts loaded within the browser’s Dev tools (inspect element) or run a search for some source functions and see what we find. In our example, we see that part of the JS code that handles the storeId parameter.

JS Snippet that handles the storeId URL parameter

var store = (new URLSearchParams(window.location.search)).get(‘storeId’);

This piece of code is looking at the URL and looking for a parameter called storeId and store the value of the parameter in the store variable. The code that follows includes a few “document.write” sinks we mentioned earlier. The following line is the one we care about the most because it’s the line that will execute our XSS Payload as HTML.

document.write(‘<option selected>’+store+’</option>’);

This line will take whatever value in the storeId parameter and add it to the page as HTML code as text between <option> element tags. We can test this by supplying a value to storeId and see how the page reacts.

Testing storeId Parameter

With all this information, we can now try passing a <script> element with some JavaScript code to the storeId parameter and see that it’s included on the page is if it were code. Note: in this case we’re just passing an alert box saying “This is Vulnerable” but the actual possibilities of what an attacker can do at this point are endless.

storeId=<script>alert(%27This%20is%20Vulnerable!%27)</script>

Vulnerability Exploited

Prevention

General prevention against any type of vulnerability that involves user input involves the use of input validation and sanitization. Input validation ensures that the data being submitted is that type of data that’s expected. As in the case of the example above, the JS code could’ve added a validation to ensure that the only values allowed for that parameter would be 1 of the 3 stores that were expected. Input sanitization ensures that whatever data is being displayed is being displayed as text and not to be confused as additional code. Our payload should have been displayed as text rather than HTML/JS code.

For more information on XSS Prevention, please see DOM based XSS Prevention Cheat Sheet

References

--

--

FATH3AD

Information security professional trying my hand at writing and sharing what I learn 💻🧠📖