SSL Certificate Analysis Open Port Detection Web Application Scanning DNS Security Audit HTTP Header Analysis Misconfiguration Detection Software Fingerprinting Subdomain Enumeration
SSL Certificate Analysis Open Port Detection Web Application Scanning DNS Security Audit HTTP Header Analysis Misconfiguration Detection Software Fingerprinting Subdomain Enumeration

All Entries

Category Client-Side Injection / Cross-Site Scripting
Typical Severity High
OWASP A03:2021 – Injection
CWE CWE-79 – Improper Neutralization of Input During Web Page Generation
Also known as DOM XSS, Type-0 XSS, Client-Side XSS
Affected systems Web applications using client-side JavaScript that reads from attacker-controlled sources (URL fragments, query strings, postMessage, localStorage, document.referrer) and writes to dangerous DOM sinks
First formalised Amit Klein, 2005 — "DOM Based Cross Site Scripting or XSS of the Third Kind"

Overview

DOM-based Cross-Site Scripting (DOM XSS) is a variant of XSS in which the vulnerability exists entirely within client-side JavaScript code rather than in server-generated HTML. The attack payload never travels to the server; instead, the browser's own JavaScript reads attacker-controlled data from a source and passes it, without adequate sanitisation, to a dangerous sink that causes script execution. Because the server never sees the payload, server-side input validation and output encoding cannot prevent the attack on their own.

DOM XSS is catalogued under CWE-79 alongside reflected and stored XSS, but its purely client-side data flow makes it distinct in both detection and remediation strategies. Sensagraph detects DOM XSS by analysing client-side JavaScript data flows between known sources and dangerous sinks.

How it works

The attack chain consists of two components: a source (where attacker-controlled data enters JavaScript) and a sink (a DOM API or property that causes code execution or HTML injection when passed unsanitised data).

Common Sources

  • location.hash — URL fragment, never sent to the server
  • location.search — query string parameters
  • location.href, location.pathname
  • document.referrer
  • window.name
  • postMessage event data
  • localStorage / sessionStorage values written by a compromised origin
  • WebSocket messages, XMLHttpRequest / Fetch responses containing attacker-influenced data

Common Dangerous Sinks

  • element.innerHTML, element.outerHTML
  • document.write(), document.writeln()
  • eval(), setTimeout(string), setInterval(string), new Function(string)
  • element.src, element.href (when set to javascript: URIs)
  • location.href, location.assign(), location.replace() (open-redirect escalation to XSS)
  • jQuery methods: $() with HTML strings, .html(), .append()

Attack Example

Consider the following vulnerable JavaScript snippet:

// Vulnerable code
const name = decodeURIComponent(location.hash.slice(1));
document.getElementById('greeting').innerHTML = 'Hello, ' + name;

An attacker crafts the URL https://example.com/page#<img src=x onerror=alert(document.cookie)> and delivers it to a victim. When the victim's browser loads the page, the fragment value is read directly into innerHTML, causing the injected script to execute in the context of example.com — without the payload ever appearing in a server request.

Why Server-Side Defences Are Insufficient

  • The URL fragment (#...) is not transmitted to the server per the HTTP specification (RFC 3986 §3.5).
  • Server-side sanitisation and WAFs operate on HTTP request data they never receive.
  • Content Security Policy (CSP) can limit impact but cannot fully prevent DOM XSS when unsafe-eval or unsafe-inline is present, or when the sink does not trigger inline script execution.

Business impact

A successful DOM XSS exploit gives an attacker arbitrary JavaScript execution in the victim's browser under the target origin. Potential consequences include:

  • Session hijacking — theft of session cookies (where HttpOnly is absent) or token-based credentials stored in JavaScript-accessible storage.
  • Credential harvesting — injection of fake login forms or keyloggers onto trusted pages.
  • Account takeover — performing authenticated actions on behalf of the victim (CSRF-equivalent via script).
  • Data exfiltration — reading sensitive page content, DOM state, or accessible APIs and exfiltrating the data to an attacker-controlled endpoint.
  • Malware distribution — redirecting victims to exploit kits or initiating drive-by downloads.
  • Regulatory exposure — breaches resulting from XSS may trigger GDPR, PCI DSS, or HIPAA notification obligations and fines.

Because DOM XSS often targets the URL fragment and leaves no server-side log trace, forensic investigation and incident response are more complex than for server-side XSS variants.

How to fix it

  1. Avoid dangerous sinks entirely where possible. Prefer DOM APIs that do not parse HTML: use element.textContent instead of element.innerHTML when inserting plain text. Use document.createElement() and appendChild() to build DOM trees programmatically.
  2. Sanitise all data before writing to HTML sinks. If HTML output is required, use a trusted, actively maintained client-side sanitisation library such as DOMPurify. Always configure it restrictively (e.g., forbid javascript: URIs).
    // Safe use of DOMPurify
    const name = decodeURIComponent(location.hash.slice(1));
    document.getElementById('greeting').innerHTML =
      'Hello, ' + DOMPurify.sanitize(name);
  3. Apply context-aware output encoding. Encode data for the specific context it is being inserted into (HTML body, HTML attribute, JavaScript string, CSS, URL). Libraries such as the OWASP Java Encoder (server-side) or DOMPurify / he.js (client-side) provide context-aware encoding.
  4. Avoid passing attacker-controlled strings to JavaScript execution sinks. Never pass user-supplied data to eval(), setTimeout/setInterval with string arguments, new Function(), or document.write().
  5. Implement a strict Content Security Policy (CSP). A CSP with script-src 'nonce-{random}' or script-src 'strict-dynamic' eliminates most inline-script-based exploitation even if a DOM XSS flaw exists. Remove unsafe-inline and unsafe-eval directives.
  6. Validate and restrict postMessage origins. Always verify event.origin against an allowlist before processing postMessage data. Never pass postMessage data directly to a dangerous sink.
  7. Audit JavaScript frameworks and third-party libraries. Many frameworks (React, Angular, Vue) provide safe-by-default data binding but expose escape hatches (dangerouslySetInnerHTML, bypassSecurityTrust*, v-html) that re-introduce DOM XSS risk if misused. Audit all usages.
  8. Conduct code review and automated SAST/DAST scanning. Use static analysis tools capable of taint tracking across JavaScript data flows to identify source-to-sink paths. Dynamic testing should include fragment-based and postMessage-based payloads.
  9. Adopt Trusted Types (where supported). The Trusted Types browser API (enforced via CSP require-trusted-types-for 'script') mandates that values written to dangerous DOM sinks pass through a defined policy, making DOM XSS injection significantly harder.

References

Frequently asked questions

In reflected and stored XSS, the malicious payload is processed and returned (or stored and later returned) by the server, and the injection point is in server-generated HTML. In DOM-based XSS, the payload never reaches the server; the browser's own JavaScript reads attacker-controlled data from a source (e.g., the URL fragment) and writes it to a dangerous DOM sink, causing execution entirely on the client side.

Not reliably. A WAF inspects HTTP request data. If the attack payload is delivered via the URL fragment (the part after #), it is never transmitted to the server in the HTTP request, so the WAF never sees it. Even for query-string-based DOM XSS, the WAF cannot analyse how client-side JavaScript subsequently handles the data. Server-side controls must be supplemented with client-side sanitisation and a strict Content Security Policy.

Modern JavaScript frameworks use safe-by-default data binding that automatically encodes output in standard interpolation contexts. However, all major frameworks expose escape hatches — React's dangerouslySetInnerHTML, Angular's bypassSecurityTrustHtml, and Vue's v-html directive — that bypass these protections and re-introduce DOM XSS risk. Applications that use these escape hatches must sanitise the data manually before passing it in.

Trusted Types is a browser API (enforced via the Content Security Policy directive require-trusted-types-for 'script') that requires values written to dangerous DOM sinks (such as innerHTML or eval) to be wrapped in a Trusted Type object produced by a developer-defined policy. This makes it structurally difficult to introduce new DOM XSS sinks and allows centralised auditing of all HTML injection points.

DOM XSS still requires the victim to load a crafted URL or receive a malicious postMessage, which typically involves social engineering (phishing, malicious link). However, unlike stored XSS — which fires automatically when any user visits a page — DOM XSS attacks are usually targeted and require delivering the payload to specific victims. Stored DOM XSS (where a value written to localStorage or a database is later read into a dangerous sink) can fire automatically on subsequent visits.