DOM XSS Through location.hash and Dangerous JavaScript Sinks
The fragment after # usually never reaches the server. JavaScript can still read it through location.hash, and single-page applications use it for routes, tabs, search state, and deep links. When that value flows into a dangerous DOM API without the right encoding, you get XSS that may leave no suspicious request in server logs.
Begin by changing the fragment to a unique marker: #BF19-HASH-72. Search the live DOM and loaded scripts for the marker, then use DevTools breakpoints or a DOM-invader tool to trace where it goes. Viewing page source is not enough because client code builds the vulnerable markup after load.
Common sinks include innerHTML, outerHTML, insertAdjacentHTML, document.write, string-based setTimeout, eval, and jQuery's .html(). Navigation assignments such as location.href = value can also become script execution through unsafe schemes. A source and sink existing in the same bundle is only a lead; prove a real data path between them.
Understand fragment parsing
Browsers expose the leading #, while frameworks may decode the rest once or twice. Test plain text, percent encoding, and separators used by the router. A payload can fail because the router strips it before the vulnerable component mounts, not because the sink is safe.
Suppose a help page does this:
const topic = decodeURIComponent(location.hash.slice(1));
result.innerHTML = "Showing: " + topic;
A harmless HTML canary proves markup injection. From there, choose an event-driven element that works in the actual browser context. Plain <script> tags inserted through innerHTML often do not execute, which causes false negatives. Do not reach for a giant polyglot when a small context-specific probe explains the flaw better.
Content Security Policy changes impact. Check whether inline event handlers, javascript: URLs, or external scripts are allowed. A strong nonce-based CSP may block your first payload, but DOM injection can still damage page integrity or become exploitable through a trusted gadget. Report what executes under the deployed policy, not what would execute if CSP vanished.
Build a victim-usable URL
DOM XSS needs a delivery path. Put the payload in a complete URL and open it in a clean browser profile. Confirm it works without DevTools edits, extensions, or a session state only your test browser has. If authentication is required, say so. If clicking an internal link rewrites the fragment before processing, capture that step.
Avoid external callbacks for the first proof. Change the document title or log a canary. If impact needs a privileged action, use two accounts you control and a benign same-origin request. The point is showing script execution in the application's origin, not collecting your own cookie.
Give developers the data flow
Document source, transformations, sink, route, and CSP behavior. A useful report says “location.hash is decoded in router.js and concatenated into results.innerHTML on /help,” then provides the minimal URL. That is faster to fix than a screenshot of an alert with no code path.
The durable fix is to use textContent for text, safe DOM construction for elements, strict URL parsing for navigation, and a reviewed sanitizer when user-controlled HTML is truly required. CSP and Trusted Types add valuable guardrails. Encoding at the server cannot repair a fragment the server never sees.