In DOM-based XSS, the problem is entirely in the page’s own JavaScript: it takes data from somewhere an outsider can influence (a source) and passes it to something that interprets it as code or HTML (a sink). The server may never see the data.
Sources: where untrusted data enters
location.search,location.hash,location.hrefdocument.referrerwindow.namepostMessagedata- Web storage and cookies (they can be set by an earlier attack, or by another script)
- API responses and anything typed by a user
Sinks: places that can execute or parse
| Sink | Why it is dangerous | Safer choice |
|---|---|---|
innerHTML, outerHTML, insertAdjacentHTML | parses HTML | textContent, createElement |
document.write() | writes raw HTML | DOM methods |
eval(code) | runs a string as code | JSON.parse for data, real functions |
new Function("...") | runs a string as code | a normal function |
setTimeout("code string", ...) | a string becomes code | setTimeout(() => ..., ...) |
element.setAttribute("onclick", ...) and inline onclick | string becomes code | addEventListener |
location = userValue or a.href = userValue | javascript: URLs | validate the URL |
script.src = userValue | loads attacker code | never from untrusted data |
iframe.srcdoc | parses HTML | avoid with untrusted data |
The pattern to avoid
// Takes a name from the URL and writes it as HTML. Do not do this.
const name = new URLSearchParams(location.search).get("name");
document.querySelector("#welcome").innerHTML = "Welcome, " + name;
The safe pattern
<p id="welcome"></p>
<script>
// Imagine this came from location.search
const name = "<img src=x> Maya";
document.querySelector("#welcome").textContent = "Welcome, " + name; // text, never parsed
</script>
Keep data as data
- Parse JSON with
JSON.parse, nevereval. - Pass functions to timers and event listeners, never strings.
- Use
datasetandsetAttributefor data attributes, not for event handlers. - If a value must be used in a URL, build it with
URL/URLSearchParams.
Check messages from other windows
postMessage data is untrusted. Always check who sent it.
window.addEventListener("message", event => {
if (event.origin !== "https://trusted.example") return; // ignore anyone else
if (typeof event.data !== "string") return;
status.textContent = event.data; // and treat it as text
});
Content Security Policy and Trusted Types
Two browser features add a safety net:
- Content Security Policy (CSP): a response header that tells the browser which scripts may run, for example banning inline scripts and
eval. Even if something slips through, the browser refuses to execute it. - Trusted Types: a browser feature (not supported everywhere) that makes dangerous sinks accept only values that went through an approved policy.
Both are configured on the server or in the page’s security headers, and they take some planning. They are excellent second layers, not replacements for writing safe code.
Review habit
When you read code, search for the sinks above. For each one ask: can an outsider influence this value? If yes, change it.
Linters help
innerHTML and eval uses automatically. Turn them on in real projects.