Back to Blog
SecurityJuly 1, 20264 min read

DOM-based XSS: How to Detect and Mitigate Vulnerabilities

DOM-based XSS happens when your client-side code executes untrusted data. Learn to detect these vulnerabilities and secure your frontend with modern practices.

DOM-based XSSJavaScriptSecurityFrontendWeb DevelopmentCSPOWASP

During a recent audit of a legacy internal dashboard, I found a classic vulnerability that had been sitting in plain sight for months. A developer had used location.hash to dynamically update a welcome message on the page, assuming that because the data never touched the server, it was "safe." It wasn't. By injecting a payload into the URL fragment, I could execute arbitrary JavaScript in the context of any user who clicked the link.

That’s the core of DOM-based XSS. Unlike reflected or stored XSS, where the server is the vector, this attack happens entirely in the browser. The vulnerability lives in the client-side code, specifically in how it handles data from an untrusted source and passes it to an execution sink.

Identifying the Flow: Sources and Sinks

To stop these attacks, you have to think like an attacker. You need to map out where data enters your application and where it gets executed.

A source is a JavaScript property that the attacker can control. Common examples include:

  • location.search
  • location.hash
  • document.referrer
  • window.name

A sink is a function or DOM object that can execute code or render HTML. If you pass data from a source to a sink without proper sanitization, you’ve created a vulnerability. Watch out for these common sinks:

  • element.innerHTML
  • document.write()
  • eval()
  • setTimeout() (when passed a string)

We previously covered Preventing DOM-based XSS: A Guide for Modern JavaScript Apps, which goes deeper into the specific browser APIs that act as high-risk sinks.

The Wrong Turn: Why "Simple" Sanitization Fails

Early in my career, I tried to mitigate these issues using regex-based filtering. I’d strip out <script> tags whenever a user input field updated the DOM. It felt clever until I realized that an attacker could bypass it with an <img> tag and an onerror attribute.

Regex is the wrong tool for DOM-based XSS because HTML is not a regular language. If you try to parse it, you will eventually lose to an edge case. Instead, focus on secure frontend development by avoiding dangerous sinks entirely.

If you must render user-provided HTML, use a battle-tested library like DOMPurify. It’s the industry standard for a reason.

JAVASCRIPT
// The dangerous way
const userContent = new URLSearchParams(window.location.search).get(CE9178">'bio');
document.getElementById(CE9178">'profile').innerHTML = userContent; 

// The secure way
import DOMPurify from CE9178">'dompurify';
const cleanContent = DOMPurify.sanitize(userContent);
document.getElementById(CE9178">'profile').innerHTML = cleanContent;

Defensive Layers for Client-Side Security

Even with good coding habits, bugs happen. You need a defense-in-depth strategy. If you're building modern interfaces, check out our Security Best Practices in React: Hardening Your Production Apps for framework-specific advice on managing data flow.

Beyond sanitization, implement these two strategies:

  1. Content Security Policy (CSP): A strong CSP is your last line of defense. By setting script-src 'self', you prevent the browser from executing any inline scripts or scripts loaded from untrusted domains, even if an attacker manages to inject them into the DOM.
  2. Use Safer APIs: Prefer textContent or innerText over innerHTML. These properties treat the input as raw text, making it impossible for the browser to interpret the data as HTML elements.
Sink TypeDangerous Property/MethodSafer Alternative
HTML RenderinginnerHTMLtextContent / innerText
URL Manipulationlocation.hrefURL API / <a> tag rel attributes
Executioneval()JSON.parse()
Stylesstyle.cssTextelement.style.property

Staying Vigilant

When I’m reviewing code, I specifically look for any data flow originating from the URL. It’s the most common source of JavaScript vulnerability reports I see today. If you’re interested in broader mitigation, we’ve also detailed XSS prevention strategies: A guide for modern web developers to help you standardize your approach across different application layers.

One thing I’m still experimenting with is the Trusted Types API. It enforces policies that prevent you from accidentally passing strings to dangerous sinks, forcing you to use trusted objects instead. It’s not supported everywhere yet, but it’s the future of browser-level protection.

Next time you’re building a feature, ask yourself: "Where does this data come from, and what happens if it contains a malicious payload?" If you can’t answer that, start by replacing your innerHTML calls. It’s a small change that saves you hours of post-incident debugging later.

FAQ

What is the difference between DOM-based XSS and Reflected XSS? Reflected XSS involves the server—the payload is sent to the server and reflected back in the response. DOM-based XSS occurs entirely on the client side, meaning the server never even sees the malicious payload.

Are modern frameworks like React or Vue immune to DOM-based XSS? Not entirely. While they automatically escape data inserted into templates, you can still introduce vulnerabilities by using "dangerouslySetInnerHTML" in React or v-html in Vue without proper sanitization.

Can I rely on CSP alone to stop DOM-based XSS? CSP is a powerful deterrent, but it’s not a silver bullet. If your application has a "gadget" (a piece of legitimate code that performs an unsafe action), an attacker might still bypass CSP. Always sanitize your inputs as your primary defense.

Similar Posts