What is DOM-based Cross-Site Scripting (XSS)? Ways to Exploit, Examples and Impact
Learn how DOM-based XSS works, explore technical exploitation examples, and find actionable mitigation strategies to secure your client-side JavaScript.
Cross-Site Scripting (XSS) remains one of the most persistent and dangerous vulnerabilities in the modern web landscape. While many developers are familiar with Reflected and Stored XSS, DOM-based Cross-Site Scripting represents a unique, client-side challenge that often evades traditional server-side security filters. In this guide, we will break down what DOM-based XSS is, how it differs from other XSS types, and provide technical examples of how it is exploited and prevented.
Understanding the Document Object Model (DOM)
To understand DOM-based XSS, we must first understand the Document Object Model (DOM). The DOM is a programming interface for web documents. It represents the page so that programs can change the document structure, style, and content. When a browser loads an HTML document, it creates a tree-like representation of the page. JavaScript can then interact with this tree to dynamically update what the user sees without requiring a full page reload.
DOM-based XSS occurs when an application contains client-side JavaScript that processes data from an untrusted source in an unsafe way, usually by writing the data back to the DOM. Unlike Reflected or Stored XSS, where the malicious payload is embedded in the HTML response by the server, DOM-based XSS is an attack where the entire vulnerability exists within the client-side code itself.
The Core Mechanics: Sources and Sinks
The vulnerability of DOM-based XSS is defined by the relationship between a "source" and a "sink."
What is a Source?
A source is a JavaScript property that accepts data that is potentially under the control of an attacker. These are entry points where malicious strings can be introduced into the application's execution flow. Common sources include:
location.search: The query string of the URL (e.g.,?name=user).location.hash: The fragment identifier (e.g.,#section1).location.pathname: The path portion of the URL.document.referrer: The URL of the page that linked to the current page.window.name: A property that persists across different pages in the same window.localStorage/sessionStorage: Data stored in the browser that might have been influenced by a previous XSS attack.
What is a Sink?
A sink is a potentially dangerous JavaScript function or DOM object that can execute code or render HTML if it receives malicious input. If data from a source reaches a sink without proper validation or escaping, an XSS vulnerability exists. Common sinks include:
- Execution Sinks: Functions that execute strings as code, such as
eval(),setTimeout(), andsetInterval(). - HTML Sinks: Properties that render HTML, such as
element.innerHTML,element.outerHTML, anddocument.write(). - Attribute Sinks: Properties that can trigger JavaScript execution through event handlers, such as
element.src(in<img>or<iframe>) orelement.href(in<a>).
How DOM-based XSS Differs from Reflected and Stored XSS
The primary distinction lies in where the payload is processed.
In Reflected XSS, the attacker sends a link to the victim containing a script. The server receives the request, includes the script in the HTML response, and the browser executes it. The server is actively involved in "reflecting" the payload.
In Stored XSS, the payload is saved on the server (e.g., in a database as a comment) and served to every user who views that content.
In DOM-based XSS, the server may never even see the malicious payload. For example, data in the URL fragment (anything after the # symbol) is typically not sent to the server in the HTTP request. The entire exploitation happens within the browser as the client-side JavaScript reads the fragment and processes it unsafely. This makes DOM-based XSS particularly difficult for Web Application Firewalls (WAFs) to detect, as the malicious traffic never passes through the server-side inspection layer.
Technical Examples of DOM-based XSS
Example 1: The innerHTML Sink
Imagine a website that greets users based on a name provided in the URL fragment. The code might look like this:
// Vulnerable Code
const name = decodeURIComponent(window.location.hash.substring(1));
document.getElementById('greeting').innerHTML = "Hello, " + name;
If a user visits https://example.com/#Alice, the page displays "Hello, Alice". However, an attacker can craft a URL like this:
https://example.com/#<img src=x onerror=alert(1)>
When the script executes, it takes the string <img src=x onerror=alert(1)> and assigns it to innerHTML. The browser then parses this string as HTML, attempts to load an image with an invalid source (x), triggers the onerror event, and executes the JavaScript alert(1).
Example 2: The eval() Sink
Some applications use eval() to parse data or execute dynamic logic. Consider a site that uses a URL parameter to determine which function to run:
// Vulnerable Code
const params = new URLSearchParams(window.location.search);
const callback = params.get("callback");
if (callback) {
eval(callback + "()");
}
An attacker can manipulate the callback parameter to execute arbitrary code. By navigating to https://example.com/?callback=alert('Hacked')//, the eval() function will execute alert('Hacked')//(). The trailing slashes comment out the remaining parentheses, allowing the injected code to run cleanly.
Example 3: jQuery and the Selector Sink
Older versions of libraries like jQuery were famously vulnerable to DOM XSS through their selector functions. If an application used the following code:
// Vulnerable Code
$(window.location.hash);
An attacker could provide a hash like https://example.com/#<img src=x onerror=alert(1)>. jQuery would attempt to find an element matching that string, but in doing so, it would create the HTML element and execute the embedded script.
Advanced Exploitation: Bypassing Filters
Attackers often encounter basic security measures, such as filters that look for the word script. To bypass these, they use various encoding techniques and alternative event handlers.
- Event Handlers: Instead of
<script>, attackers useonmouseover,onload,onerror, oronclick. - Encoding: Using URL encoding or HTML entities can sometimes bypass simple string-matching filters. For example,
alert(1)can be written in hex or decimal entities. - Template Literals: In modern JavaScript, backticks and
${}can be used to construct payloads that evade filters looking for standard quotes.
The Impact of DOM-based XSS
The impact of a successful DOM-based XSS attack is identical to other forms of XSS, which is to say, it is severe. An attacker can:
- Steal Session Cookies: By accessing
document.cookie, an attacker can hijack a user's session and impersonate them on the site. - Exfiltrate Sensitive Data: The script can read data from the DOM, such as CSRF tokens, personal information, or financial details, and send it to an external server.
- Perform Actions on Behalf of the User: The script can trigger HTTP requests (e.g., changing a password or making a purchase) using the user's authenticated session.
- Phishing and Defacement: The attacker can modify the DOM to display fake login forms or misleading information to trick the user into revealing credentials.
How to Prevent DOM-based XSS
Preventing DOM-based XSS requires a shift in mindset: treat all client-side data as untrusted, just as you would on the server.
1. Avoid Dangerous Sinks
The most effective way to prevent DOM XSS is to avoid using dangerous sinks.
- Instead of
element.innerHTML, useelement.textContent.textContenttreats all input as literal text and will not execute HTML or scripts. - Instead of
eval(), useJSON.parse()for data handling or direct logic for function calls. - Avoid
document.write()entirely in modern web development.
2. Sanitize Data Before Insertion
If you must render HTML dynamically, use a dedicated sanitization library like DOMPurify. These libraries parse the input and strip out dangerous tags and attributes while keeping safe HTML.
// Secure Code using DOMPurify
const cleanHTML = DOMPurify.sanitize(untrustedInput);
document.getElementById('content').innerHTML = cleanHTML;
3. Implement a Strong Content Security Policy (CSP)
A Content Security Policy (CSP) is a powerful defense-in-depth mechanism. By defining which scripts are allowed to run, you can mitigate the impact of XSS even if a vulnerability exists. A policy that restricts script-src to trusted domains and disallows 'unsafe-inline' and 'unsafe-eval' can block most DOM-based XSS exploits.
4. Use Safe APIs for Navigation
When handling redirects or link updates, avoid concatenating strings directly into window.location. Validate the destination URL against an allowlist to prevent javascript: URI injection.
Conclusion
DOM-based Cross-Site Scripting is a sophisticated vulnerability that highlights the complexity of modern client-side development. By understanding the flow of data from sources to sinks, developers can identify and neutralize these threats before they reach production. As web applications move more logic to the browser, the importance of secure JavaScript coding practices only grows.
To proactively monitor your organization's external attack surface and catch exposures before attackers do, try Jsmon.