DOM-based cross-site scripting

What is DOM-based cross-site scripting?

DOM-based cross-site scripting is a specific type of cross-site scripting (XSS) vulnerability. The term DOM-based means that the attack takes advantage of the Document Object Model (DOM).

More information about DOM-based cross-site scripting

The DOM is an internal data structure that stores all of the objects and properties of a web page. For example, every tag used in HTML code represents a DOM object. Additionally, the DOM of a web page contains information about such properties as the page URL and meta information. Developers may refer to these objects and properties using JavaScript and change them dynamically.

The Document Object Model is what makes dynamic, single-page applications possible. However, it is also what makes DOM-based cross-site scripting possible.

Unlike all other types of cross-site scripting, DOM-based XSS is purely a client-side vulnerability. This means that during a DOM-based XSS attack, the payload never reaches the server. The entire attack happens in the web browser.

DOM-based XSS is similar to reflected XSS because no information is stored during the attack. A DOM-based XSS attack is also conducted by tricking a victim into clicking a malicious URL.

Sources and sinks in DOM-based cross-site scripting

Every DOM-based XSS vulnerability has two elements: the source of user input and the target where this user input is written, called a sink. Popular sources that attackers can manipulate are document.URL, document.documentURI, location.href, location.search, location.*, window.name, and document.referrer. Popular sinks are document.write, (element).innerHTML, eval, setTimeout, setInterval, and execScript. Note that this list is not exhaustive and many other sources and sinks also exist.

For JavaScript code to be vulnerable to DOM-based XSS, it must take information from a source that can be controlled by the attacker and then pass this information to a sink.

Example of DOM-based cross-site scripting

In this example, the developer wants to display the name of the user on the dashboard page (dashboard.html). The name of the user is passed to the application as a parameter in the URL:

<html>
(...)
Dashboard for
<script>
   var pos=document.URL.indexOf("context=")+8;
   document.write(decodeURIComponent(document.URL.substring(pos)));
</script>
(...)
</html>

The inline script looks for context= in the URL (document.URL.indexOf("context=")), takes all the text to the right of it (+8 means 8 characters to the right of the beginning of context=), and uses document.write to insert that text directly into HTML to be interpreted by the browser.

If you call the following URL:

http://www.example.com/dashboard.html?context=Thomas

the page will say:

Dashboard for Thomas

The DOM-based cross-site scripting attack

The attacker creates the following URL:

http://www.example.com/dashboard.html?context=
%3c%73%63%72%69%70%74%3e%61%6c%65%72%74%28%22%4c%45
%41%56%45%20%54%48%49%53%20%50%41%47%45%21%20%59%4f
%55%20%41%52%45%20%42%45%49%4e%47%20%48%41%43%4b%45
%44%21%22%29%3b%3c%2f%73%63%72%69%70%74%3e

The long string of hex codes in this payload is a URL-encoded form of the following content:

<script>alert("LEAVE THIS PAGE! YOU ARE BEING HACKED!");</script>

Then, the attacker sends the URL to the victim, for example, in an email or instant message. The victim clicks the URL, causing their browser to open the dashboard.html page and run the malicious script. This rewrites the document content and inserts the following tag into the HTML interpreted by the browser:

Dashboard for <script>alert("LEAVE THIS PAGE! YOU ARE BEING HACKED!");</script>

As a result, the browser displays a pop-up window urging the user to leave the page. The consequences are that targeted users will stop visiting the web application, fearing for their safety.

The fix

Informed of the vulnerability, the developer rewrites the code using a safe sink. As a result, untrusted content from the source will always be interpreted as text, not code:

<html>
(...)
Dashboard for <span id="contentholder"></span>
<script>
   var pos=document.URL.indexOf("context=")+8;
   document.getElementById("contentholder").textContent = 
       document.URL.substring(pos,document.URL.length);
</script>
(...)
</html>

The developer creates a placeholder object and writes the user’s name not into HTML directly but into the textContent property of the placeholder object (using a safe sink). This guarantees that the browser won’t interpret this content as code and will simply display it as text.

Consequences of DOM-based cross-site scripting attacks

DOM-based cross-site scripting vulnerabilities are not very common but the consequences of a successful attack can be as dire as those of other reflected XSS attacks. Here are some actions that a black-hat hacker could perform based on the simple example presented earlier:

  • They could create a phishing campaign and send millions of emails containing a malicious link with a payload that redirects users to a phishing page designed to mimic your web application. As a result, millions of users could potentially have their credentials stolen and blame your web application, which would severely damage your reputation.
  • They could create a payload that sends the user to a malicious page that imitates your application’s login page. Then, they could send this malicious URL to your internal users, even your CEO. If even one of your users falls for the trick, the attacker will obtain their credentials to escalate the attack. Ultimately, this could let malicious actors gain access to other computer systems in your organization.

How to detect DOM-based cross-site scripting vulnerabilities

Due to the unique nature of DOM-based XSS vulnerabilities, many web application security tools fail to detect them. This is the case for tools that focus on server-side code and analyzing HTTP requests but are unable to scan scripts executed in the browser. For example, most SAST and IAST tools are made to scan specific server-side languages and ignore JavaScript code. Therefore, to cover threats related to DOM-based XSS, you need manual penetration testing or professional DAST (or dynamic IAST) scanners that use an embedded browser engine to test for DOM-based XSS.

How to prevent DOM-based cross-site scripting vulnerabilities

The best way to completely avoid DOM-based XSS vulnerabilities in your JavaScript code is to use the correct output method (a safe sink). For example, if you want to write into a <div> element, don’t use innerHtml. Instead, use innerText or textContent.

Note that not all DOM elements have a safe output method. There are cases when you must simply avoid using untrusted data. For example, you must never write any untrusted data to sinks such as eval or execScript.

You can also use typical XSS protection techniques (filtering and escaping) applied to JavaScript. Unfortunately, unlike for server-side languages, there are no universal JavaScript libraries to help you filter and escape data, so developers need to write and maintain such functionality themselves. For DOM-based XSS, proper filtering and escaping is a complex issue, described in detail in a dedicated OWASP cheat sheet.

How to mitigate DOM-based cross-site scripting vulnerabilities

Unlike other cross-site scripting vulnerabilities, you cannot mitigate DOM-based XSS using a web application firewall (WAF) or generic framework protection like request validation in ASP.NET. Such mechanisms are completely useless against DOM-based XSS attacks because the payload never reaches the server.

There is no way to mitigate or temporarily prevent zero-day DOM-based XSS attacks other than paying attention to the code of your web applications, scanning all your applications as often as possible, and keeping your third-party applications updated.

Related blog posts