Finding and Fixing DOM-XSS

Published: 19 October 2020

Introduction

We’ve previously written about Reflected and Stored Cross-site Scripting, however this time we want to tackle DOM-Based Cross-site Scripting, or DOM-XSS for short. The exploitation of DOM-XSS is frequently very similar to Reflected Cross-site scripting, were the payload is stored within the URL and exploitation occurs where a user can be tricked into clicking the link, such as through a phishing email – but we’ll break it down step by step.

Cross-site Scripting vulnerabilities occur where scripts can be executed within another user’s view of a web application. It can allow for attacks such as virtual defacement of the page, the theft of confidential data, or the distribution of malicious software to users of the site.

With Reflected or Stored XSS, user input is insecurely embedded within the HTML source of the page which can lead to JavaScript execution. However with DOM-XSS a vulnerable script itself allows user input to be executed as code within the script, which means the attack is entirely client-side (and occasionally the payload is not even sent to the server).

Finding and Exploiting DOM-XSS

The trick to finding DOM-XSS is finding a dangerous function which accepts user input. It sounds easy at first, but the user input can be quite distance from the dangerous function within a long script file.

Examples of dangerous JavaScript functions include:

document.execCommand()
document.write()
document.writeln()
document.evauate()
eval()
setInterval()
setTimeout()
[element].innerHtml
jQuery.globalEval() /* requires jQuery */

If these functions take user controlled input, a vulnerability could occur. User input includes variables such as:

document.location
document.location.href
document.location.hash
document.documentURI
document.baseURI
document.URL

For example, here is a very simple vulnerable script:

<script>
var stuff = unescape(location.hash.substr(1));
document.write("Your input was: "+stuff+"<br><br>");
</script>

The script is using location.hash, which is user input (it’s the part of the web address following the hash symbol, which can crafted through a malicous link). By supplying location.hash to the dangerous function document.write() without filtering the input, a DOM-XSS vulnerability occurs.

To demonstrate this we can show an example. Here we have an example on our workshop page, which we use for our security training courses. This page includes the short script given above and therefore can be exploited through a crafted link, such as:

https://akimboworkshop.com/challenges/dom-xss/1/?#<script>alert()</script>

The result is a JavaScript alert() box, which demonstrates it’s possible execute JavaScript by tricking a user into clicking the link.

The example payload causes the JavaScript alert() function to execute within the browser

The issue occurs in this case as the vulnerable script reads the contents of the hash through the variable location.hash. For the link above the contents of location.hash is:

#<script>alert()</script>

The substr(1) in the vulnerable script simply causes the first character (the hash) to be removed. This result is given to the dangerous function document.write() which writes the script to the page – causing the JavaScript execution.

It’s worth noting in this example, exploitation is not happening through a URL parameter being reflected by the server, as generally seen with Reflected XSS. Instead, the input is being loaded directly through JavaScript. For this example (but not necessarily all DOM-XSS) the payload will not even be sent to the server, as it is placed after the fragment (the hash in the URL) which is handled client-side only.

This is a simple proof-of-concept that shows, should a user click a link then JavaScript execution can occur – this is useful for penetration testing or bug bounty reports, however, there are many more interesting payload which could be used instead of a simple alert box. Here’s an example of a fake login page:

A fake login box created through a proof of concept malicious script.

https://akimboworkshop.com/challenges/dom-xss/1/?#<style>::placeholder { color:white; }</style><script>document.write("<div style='position:absolute;top:100px;left:250px;width:400px;background-color:white;height:230px;padding:15px;border-radius:10px;color:black'><form action='https://example.com/'><p>Your sesion has timed out, please login again:</p><input style='width:100%;' type='text' placeholder='Username' /><input style='width: 100%' type='password' placeholder='Password'/><input type='submit' value='Login'></form><p><i>This login box is presented using XSS as a proof-of-concept</i></p></div>")</script>

Fixing DOM-XSS

Whilst Reflected and Stored XSS can generally be addressed through server-side user input encoding (such as through the PHP htmlentities() function) or with browser protections such as Content-Security-Policy – this is not sufficient for DOM-XSS.

Where a dangerous function is used, user input into that function should be limited through user input filtering. An allow-list approach of restricting user input to only known-good input should be used. For example, limiting input to the smallest number of characters possible (such as alphanumerics only) and checking the expected data type (such as limiting input to integers only).

This is in contrast to a block-list of known-bad inputs being blocked, which is often less effective due to the large degree of flexibility that JavaScript allows. For a good example of this flexibility, consider something like JScrewIt.

Furthermore, dangerous functions such as eval() should be entirely avoided. The Mozilla Developer Network describes eval() as “an enormous security risk”. Their page has an entire section titled “Never use eval()!

Read More