JavaScript › DOM Manipulation

The DOM and its dangerous sinks

5 min read Intermediate 5 sections

The DOM (Document Object Model) is the live, in-memory representation of a web page that JavaScript can read and change. Understanding how JavaScript writes to the DOM is the key to understanding DOM-based XSS — one of the most common client-side vulnerabilities. The whole bug class comes down to one distinction: safe ways to insert content versus unsafe ones.

You'll learn to

  • Read and change the page through the DOM
  • Tell safe content insertion from unsafe
  • Recognise the sinks where DOM XSS lives

Reading and changing the page

// Find elements:
document.getElementById("username");
document.querySelector(".error-message");
document.querySelectorAll("a");          // all links

// Read and change them:
const el = document.querySelector("#greeting");
el.textContent = "Hello, " + name;       // SAFE: sets text only

textContent sets text and only text. If name contains <script>, it appears literally as the characters <script> on the page — it is not executed. That’s the safe path.

The dangerous sinks

A “sink” is a place where data flows into something powerful. These DOM sinks treat their input as HTML or code, not plain text:

el.innerHTML = userInput;        // parses input as HTML — DANGEROUS
el.outerHTML = userInput;        // same danger
document.write(userInput);       // writes HTML to the page — DANGEROUS
el.insertAdjacentHTML("beforeend", userInput);  // DANGEROUS

eval(userInput);                 // runs input as JavaScript — VERY DANGEROUS
setTimeout(userInput, 100);      // runs string input as code — DANGEROUS
location = userInput;            // can become javascript: URLs — DANGEROUS

The difference between textContent and innerHTML is the entire bug. textContent displays input as text; innerHTML parses it as HTML, so an attacker’s <img src=x onerror=alert(1)> becomes a real element that runs code.

A complete DOM-XSS pattern

// VULNERABLE — reads the URL fragment and writes it as HTML:
const msg = location.hash.slice(1);          // source: the part after #
document.querySelector("#out").innerHTML = msg;  // sink: innerHTML

// An attacker sends a victim this link:
// https://site.com/page#<img src=x onerror=alert(document.cookie)>
// → the onerror handler runs, stealing the cookie

Read it as a flow: data comes from a source you control (the URL hash), travels through the code, and lands in a sink that executes it (innerHTML). Source → sink with no sanitisation between them is the vulnerability.

Checkpoint

A page does element.innerHTML = location.search. Why is this dangerous, and what's the source and the sink?

Try it yourself

In the browser console on a test page, create a div and try both: div.textContent = "<b>hi</b>" versus div.innerHTML = "<b>hi</b>". Observe that one shows the literal tags as text and the other renders bold. That single difference is the heart of DOM XSS.

Summary

The DOM is the live page that JavaScript reads and writes. textContent inserts text safely; the dangerous sinks (innerHTML, outerHTML, document.write, insertAdjacentHTML, eval, string setTimeout, location) treat input as HTML or code. DOM-based XSS occurs when an attacker-controllable source (URL hash, search, document.URL) reaches one of these sinks unsanitised. Audit by finding sinks and tracing back to sources; fix with textContent or DOMPurify.

Key takeaways

  • textContent is safe (text only); innerHTML parses HTML — that’s the whole bug.
  • Dangerous sinks: innerHTML, document.write, eval, string setTimeout, location.
  • DOM XSS = attacker-controllable source → dangerous sink, unsanitised.
  • Audit by finding the sink, then tracing back to the source.

Quick quiz

Next module, the security deep dive: the full XSS family, prototype pollution, JWTs, CSP, CORS, and the rest of the client-side vulnerability catalogue.

Was this lesson helpful?