DOM XSS is similar to normal XSS in that untrusted data from the client does not get sanitized before being displayed. The big difference, however, is where the vulnerability occurs. Traditional XSS occurs when server-side code attempts to modify the DOM, but fails to keep out malicious code. DOM XSS is similar, but the failure to stop malicious code happens client-side, usually due to improper use of javascript libraries meant to manipulate the DOM. There are plenty of other differences, and a number of reasons why DOM XSS can be more harmful than traditional XSS.

How does DOM XSS occur?

Like in any programming language, there exist functions which can be used unsafely and introduce bugs. One common occurrence in javascript is creating an element with some attribute containing data supplied by the user. Like in traditional XSS, if there is no filtering, it is trivial to break out of the attribute and insert our own, or even begin adding our own elements entirely.

Take for example jQuery: jQuery is a javascript library that simplifies HTML manipulation, ajax, etc. It makes dealing with HTML so easy, it’s not surprising people can overlook DOM XSS. jQuery has something called a selector which makes finding elements on a page much easier (think of a more robust version of document.getElementById). It has a lesser known side-effect; if the element you’re searching for doesn’t exist, jQuery will actually create it for you. Combined with user input, it’s easy to see how DOM XSS can happen.

Code Examples

Example 1: Here’s an actual piece of code I have seen before illustrating this point:

    hash = location.hash.substring(1);
    if (!$('a[name|="' + hash + '"]')[0]) {
        // not important

In javascript, location.hash.substring(1) contains all the data after the “#” (fragment identifier) in the URL. So /example.html#hashtag would have a value of “hashtag.” We can see that with jQuery’s selector, we are searching for an <a> tag with a name of the value in the hashtag. This looks harmless, until we try breaking out of the selector’s syntax, and insert a tag of our own. One of the more obvious things to try out would be setting hash to

    " <script>alert(1)</script>

This will not work. This is because although we managed to create a script tag, it wasn’t appended to the DOM yet. Let’s try something a little less obvious:

    " <img src=nonexistent onerror=alert(1)>

This will cause an alert. There are minor differences from the previous payload in the ordering of elements and which events execute after being created, but that’s another story for another day. More importantly, we have just achieved javascript execution through a seemingly harmless if-exists statement.

Example 2: A similar real-world example, but no external libraries this time:

    button.onclick = function(e) {
        minimum.innerHTML = parameters.minimum;
        maximum.innerHTML = parameters.maximum;

The DOM XSS vulnerability occurs with the .innerHTML. In this context, “parameters” is an object constructed from the GET request. What’s special about this example is that the GET parameters were completely sanitized on the server-side - all HTML stripped and casted into integers. The catch here was that the same sanitization did not occur on the client-side. Because the minimum and maximum values were taken from the querystring and used blindly, we could insert our own HTML, and it would have been inserted into the DOM. By looking at the server-side code alone, we would have never known.

Finding DOM XSS

When looking at DOM XSS vulnerabilities, there is usually a list of recurring features. Some of these include functions being used improperly, or browser-supplied data that is assumed to be cleaned. I’ve assembled a list of things to watch out for, which can make static analysis a little easier.

Insecure Inputs

  • document.URL *
  • document.location.pathname *
  • document.location.href *
  • *
  • document.location.hash
  • document.referrer *
  • document.cookie

Most of the document.* variables can be accessed through the window object as well. The * indicates the value is urlencoded; the script usually must urldecode these values before a vulnerability can be introduced.

Insecure creation/modification of html elements

  • Modifying .innerHTML, .outerHTML
  • createElement
  • document.write/document.writeln
  • jQuery’s selector, $()
  • jQuery’s .html() function

Functions which evaluate strings as code

  • eval
  • execScript
  • setTimeout/setInterval
  • location.replace/location.assign
  • Anything that modifies src attributes of <script> tags
  • Anything that modifies event handlers

Like with server-side code, anything that executes code from user input is a big red flag, and is worth investigating.

Why DOM XSS can be more dangerous than traditional XSS

  1. DOM XSS can occur anywhere that javascript executes on a page.
    • This gives more flexibility in avoiding detection from a WAF, etc. For example, a common location for DOM XSS payloads is the fragment identifier (everything after # in a URL). This is a good spot for a payload, as the fragment identifier is never sent to the server.
  2. Anti-XSS filters built into browsers become ineffective.
    • DOM XSS is similar to reflected XSS, but is not easily detectable as payloads are not directly reflected into the source, but instead are part of the javascript logic.
  3. More room for vulnerabilities.
    • The parts of the HTTP request (namely the POST/GET parameters) are no longer the main targets. As mentioned earlier, DOM XSS can occur anywhere that javascript executes. This means any element on the page which has a javascript associated with it is a viable target.
  4. It has to work.
    • There are cases where traditional reflected or stored XSS exist but payloads cannot run, for example, due to a Content Security Policy header. This is not the case for DOM XSS. This is because the same policies which allow the safe javascript to run also apply to the exploit.

Further Reading