This article explains DOM-based XSS through a practical lab exercise where attackers inject malicious code via the URL fragment to trigger JavaScript execution in the browser. The vulnerability exists entirely on the client side—the server never sees the dangerous input because URL fragments (the part after #) are not sent to servers—making it invisible to server-side firewalls and filters. Understanding DOM XSS is critical because modern web applications perform significant logic in the browser, creating widespread exposure to this attack vector.
- How the QuickDocs vulnerability worksThe application reads the URL fragment using location.hash and inserts it directly into the page using innerHTML without escaping or sanitizing the content. Because innerHTML parses any markup you provide as actual HTML and DOM, an attacker can inject elements like img tags with error handlers that execute JavaScript when the page loads.
- Why server-side defenses completely miss itURL fragments after the # symbol never reach the server—browsers handle them entirely client-side. This means server-side filters, WAFs, and security tools have no visibility into the payload and cannot block it. The vulnerability lives purely in the JavaScript code, so the fix must be implemented in the browser code, not on the backend.
- Attack execution using image error handlersThe classic payload uses an img element with a broken source and an onerror event handler that automatically triggers when the browser fails to load the image. The handler can call the page's solve function or perform any other malicious action, demonstrating that injected elements and their event handlers fire immediately upon insertion into the DOM.
- Real-world prevalence and scopeDOM XSS is widespread in modern applications because so much code runs in the browser rather than on servers. Fragment-based, location-based, and postMessage-based vulnerable sinks are common findings in bug-bounty programs, and the attack can steal session tokens, perform unauthorized actions, or rewrite page content just like reflected or stored XSS.
- Proper fix: textContent instead of innerHTMLThe primary defense is to use textContent when displaying user-controlled text instead of innerHTML, since textContent writes plain text that cannot be parsed as HTML or execute code. If HTML construction is necessary, input must be sanitized using a trusted library like DOMPurify before insertion.
- Multi-layered defense strategyBeyond textContent and sanitization, developers should implement Content-Security-Policy headers as a backstop to prevent injected inline scripts from running. Additionally, developers should grep their front-end code for dangerous sinks like innerHTML, document.write, and insertAdjacentHTML, then trace whether these functions receive data from the URL, fragment, or postMessage.
- Threat modeling: treat client-side sources as untrustedThe URL, URL fragment, postMessage data, and browser storage can all be attacker-controlled and should never be trusted. Even though these sources appear local and under user control, any data flowing from them into a dangerous sink creates an exploitable DOM XSS vulnerability.
Bottom line: DOM-based XSS occurs when JavaScript unsafely writes untrusted client-side input (typically from the URL fragment) into the page using innerHTML, and this attack bypasses all server-side defenses because the vulnerable data never reaches the server. The fix is to use textContent for user text, sanitize HTML with libraries like DOMPurify if HTML must be constructed, and implement Content-Security-Policy as an additional layer of protection.
📋 In this walkthrough
What this lab teaches
DOM-based XSS lives entirely in the browser. The vulnerable code takes attacker-controlled input from a client-side source (here, the URL fragment) and passes it to a dangerous sink (here, innerHTML) without escaping it.
The lesson: because the fragment after # never reaches the server, server-side filters and most WAFs are blind to this. The bug is in the JavaScript, and the fix has to be there too.
How QuickDocs highlights your search
The page reads location.hash and inserts it into the article to highlight your search term. It builds HTML from that value and assigns it with innerHTML, so any markup you put in the fragment becomes real DOM — including an element that runs script.
The attack approach
- Find where your input appears on the page — the search term is pulled from the URL fragment.
- Confirm the fragment is written as HTML, not as plain text, by seeing whether markup renders.
- Edit the URL in the lab's address bar and put an element in the fragment that triggers JavaScript when it loads.
- Aim for the element to call the lab's solve function.
Full walkthrough (spoiler)
If markup is rendering but nothing fires yet, open the steps below.
Show the step-by-step solution
- In the iframe's address bar, change the text after the # to some HTML and reload — notice it appears as real page content, not escaped text.
- Swap in an element that runs JavaScript automatically on load (an image element with a broken source and an error handler is the classic choice).
- Have that handler call the page's solve function,
window.seLabSolve(...), with the flag the lab expects. - Load the URL. The fragment is written into the DOM, the handler fires, and the lab marks you solved.
Why it works
The page trusts the URL fragment and pipes it into innerHTML, which parses whatever you give it as HTML. Inserted elements with event handlers run immediately. Because the fragment is client-side only, nothing on the server ever had a chance to sanitise it — the entire flaw is in one unsafe DOM write.
Where this happens in the real world
DOM XSS is widespread because modern apps do so much in the browser. Fragment-, location– and postMessage-based sinks are common findings in bug-bounty programmes, and they slip past server-side defences precisely because the data never leaves the client. XSS as a class sits under Injection (A03) in the OWASP Top 10 (2021).
How to defend against it
- Use textContent, not innerHTML, whenever you display user-controlled text.
- If you must build HTML, sanitise with a trusted library (such as DOMPurify) before inserting it.
- Treat client-side sources as untrusted — the URL, the fragment,
postMessagedata and storage can all be attacker-controlled. - Set a Content-Security-Policy as a backstop so injected inline script is less likely to run.
Try it, then go further
Reading about it is the easy part. Open the lab, try your own approach before peeking at the spoiler, and see how many attempts it takes.
