Debugging DOM XSS means following data, not guessing payloads
DOM XSS testing is about sources, transformations, and sinks. Payload lists help only after the dataflow is understood.
DOM XSS is where payload spraying becomes especially weak.
The vulnerable line might not be in the server response. It might be in a bundle loaded after hydration, behind a feature flag, triggered by a hash change, or hidden in a component that only renders after an API call. The payload does not matter until you know where the data goes.
Follow the data.
Start with sources
Common sources are predictable:
location.search
location.hash
document.referrer
window.name
localStorage
postMessage
Predictable does not mean easy. A value may enter through location.hash, get parsed into an object, stored in state, passed through a router, then rendered three components later. Modern frontend code is full of indirection.
Search for obvious sinks, but do not stop there.
Sinks are not always named dangerously
The classic sink is innerHTML.
preview.innerHTML = value;
Real code often wraps it:
renderMarkdown(userInput)
setHtml(description)
dangerouslySetInnerHTML={{ __html: html }}
insertAdjacentHTML("beforeend", row)
Sometimes the dangerous operation is inside a dependency. Sometimes the app builds a DOM node safely at first, then later serializes and reparses it. That second parse is where mutation XSS can appear.
Breakpoints beat vibes
Use the browser debugger. Put breakpoints on setters when possible. Monkey patch sinks in a local test session if you need visibility:
const original = Element.prototype.insertAdjacentHTML;
Element.prototype.insertAdjacentHTML = function(position, html) {
console.trace("insertAdjacentHTML", position, html);
return original.call(this, position, html);
};
This is ugly. It is also effective.
Once you see the sink, the payload choice becomes much less mysterious. If the value is assigned to textContent, markup payloads are irrelevant. If it reaches innerHTML after one URL decode, encoding strategy matters. If it lands inside an SVG subtree, HTML assumptions may fail.
Watch the transformations in between. A value that starts as %3Cimg%3E may be decoded by URLSearchParams, escaped by a template helper, then decoded again by a Markdown preview. Another value may be lowercased, trimmed, or split on spaces before it reaches the sink. Those steps decide whether a payload needs entity encoding, URL encoding, quote breakout, or a completely different shape.
This is why copying the final working string without the path is weak evidence.
Report the path
A DOM XSS report should include the source, transformations, sink, and trigger. Developers need to reproduce the path without reading your mind.
Bad:
XSS in search page.
Better:
q parameter is read from location.search, decoded by URLSearchParams,
stored in SearchPreview state, and assigned to innerHTML in PreviewCard.
That is the difference between a bug report and a payload screenshot.