Now that you have mastered Reflected and Stored XSS, we tackle the most elusive variant: DOM-based XSS. Unlike the previous types, DOM-based XSS vulnerabilities exist entirely in client-side JavaScript code. The server may never see or process the malicious payload — the attack happens entirely within the browser's Document Object Model (DOM).
This makes DOM-based XSS invisible to traditional server-side security tools, WAFs (Web Application Firewalls), and network-level scanners. It requires understanding how JavaScript manipulates the DOM and how user-controlled data flows from sources to sinks.
DOM-based XSS is best understood through the concept of sources and sinks. A source is any JavaScript property that can be controlled by an attacker. A sink is a JavaScript function or property that can cause JavaScript execution if it receives malicious data.
| Category | Examples |
|---|---|
| Sources (Attacker-Controlled Input) | document.location, document.URL, document.referrer, document.cookie, window.name, location.hash, location.search, postMessage data, localStorage, sessionStorage |
| Sinks (Dangerous Execution Points) | eval(), document.write(), innerHTML, outerHTML, onevent handlers, setTimeout(string), setInterval(string), new Function(), jQuery $(), .html(), .append(), .after() |
A DOM-based XSS vulnerability exists when data flows from a source to a sink without proper sanitization. The key insight is that the server is often completely uninvolved — the payload may be in the URL fragment (after the #), which is never sent to the server.
// Classic DOM-based XSS example
// URL: https://example.com/page#<img src=x onerror=alert(1)>
// Client-side JavaScript:
var hash = location.hash.substring(1); // SOURCE: gets the fragment
document.getElementById('content').innerHTML = hash; // SINK: writes to DOM
// The fragment (#...) is NEVER sent to the server
// Server-side WAFs and logging never see the payload
// The attack happens entirely in the browserLet's examine the most common patterns where DOM-based XSS occurs in real-world applications.
// Pattern 1: innerHTML assignment from URL parameter
var params = new URLSearchParams(location.search);
var name = params.get('name');
document.getElementById('greeting').innerHTML = 'Hello, ' + name;
// Attack: ?name=<img src=x onerror=alert(1)>
// Pattern 2: document.write from location hash
var content = location.hash.slice(1);
document.write('<div>' + content + '</div>');
// Attack: #<script>alert(1)</script>
// Pattern 3: eval() with user input
var config = location.hash.slice(1);
var settings = eval('(' + config + ')');
// Attack: #'},alert(1),({//
// Pattern 4: jQuery $() selector from URL (older jQuery)
var target = location.hash;
$(target).show();
// Attack: #<img/src=x onerror=alert(1)>
// jQuery interprets strings starting with < as HTML
// Pattern 5: postMessage handler without origin validation
window.addEventListener('message', function(e) {
document.getElementById('result').innerHTML = e.data;
});
// Any origin can send a malicious postMessage⚠️ The jQuery $() selector sink was particularly dangerous in jQuery versions before 3.0. Passing user-controlled data to $() could cause HTML parsing and script execution. This affected millions of websites. Always validate and sanitize data before passing it to jQuery functions.
Finding DOM-based XSS requires tracing data flow from sources to sinks through the JavaScript code. Here is a systematic methodology:
// Complex data flow example
// URL: https://app.com/dashboard#section=profile
// Source: location.hash
var hash = location.hash;
// Data flows through a parser function
function parseHash(h) {
var parts = h.split('=');
return {
key: parts[0].replace('#', ''),
value: parts[1]
};
}
var parsed = parseHash(hash);
// Data flows to a router function
function routeToSection(section) {
if (section === 'profile') {
loadProfile();
} else {
// SINK: innerHTML with attacker-controlled data
document.getElementById('content').innerHTML =
'<h1>Section: ' + decodeURIComponent(parsed.value) + '</h1>';
}
}
routeToSection(parsed.key);
// Attack: #section=<img src=x onerror=alert(1)>
// The 'key' is 'section' (truthy check passes)
// The 'value' flows unsanitized into innerHTML💡 Browser developer tools are your best friend for DOM-based XSS testing. Use the JavaScript debugger to set breakpoints at sinks and trace backward to find the sources. The 'DOM Invader' extension by PortSwigger automates much of this process.
Let's exploit a real-world-style DOM-based XSS vulnerability in a single-page application that uses the URL hash to determine which content to display.
// Vulnerable SPA code (simplified)
// File: app.js
window.addEventListener('hashchange', renderPage);
window.addEventListener('load', renderPage);
function renderPage() {
var page = location.hash.slice(1) || 'home';
var template = document.getElementById('tpl-' + page);
if (template) {
document.getElementById('main').innerHTML = template.innerHTML;
} else {
// Fallback: display the page name directly
document.getElementById('main').innerHTML =
'<h2>Page not found: ' + page + '</h2>';
}
}
// Attack URL: https://app.com/#<img src=x onerror=alert(document.cookie)>
// Since no template with ID 'tpl-<img...>' exists,
// the fallback branch executes, injecting our payload into innerHTMLDOM-based XSS is particularly dangerous because it bypasses most traditional security controls:
This is why DOM-based XSS requires manual code review or specialized client-side analysis tools. It is the blind spot in most application security programs.
You now have a solid understanding of all three XSS types. In the next lesson, we will level up your skills with advanced payload crafting techniques — learning to create payloads that work across different contexts and achieve specific exploitation objectives.
Verify exercises to earn ★ 160 XP and unlock next lab level.