// https://app.example/welcome#<img src=x onerror=alert(document.cookie)>
const name = decodeURIComponent(location.hash.slice(1));
document.getElementById("greet").innerHTML = "Hi " + name;
DOM-based XSS happens fully inside the browser. Your own JavaScript reads data the attacker controls (the source) and passes it to an API that turns a string into HTML or code (the sink). In the example, location.hash is the source and innerHTML is the sink. The part after # is never sent to the server, so server-side filters never see the payload.
Common sources:
location.href,location.searchandlocation.hashdocument.referrerandwindow.namepostMessagedata from other windows- data from your API that another user wrote, such as a comment
Common sinks:
- HTML sinks:
innerHTML,outerHTML,insertAdjacentHTML,document.write, and jQuery's.html() - Code sinks:
eval,new Function, andsetTimeoutorsetIntervalcalled with a string - URL sinks:
a.href,iframe.srcandlocation.href, which run ajavascript:URL
Fixes:
- Use safe APIs:
textContentinstead ofinnerHTML, andcreateElementplussetAttributeinstead of building HTML strings. - Check that URLs start with
https:orhttp:before you use them. - When you really need HTML, sanitize it with a library like DOMPurify.
- Turn on Trusted Types, so the browser rejects plain strings at the dangerous sinks.
- A source is where attacker-controlled data enters, such as
location.hashorpostMessagedata. - A sink is an API that turns a string into HTML, code or a navigation, such as
innerHTMLoreval. - DOM-based XSS can happen without the payload ever reaching the server.
- URL sinks like
hrefcan runjavascript:URLs. - The fix is safe APIs like
textContent, URL scheme checks and sanitizing when HTML is needed.