Skip to content

DOM-Based XSS — document.write via location.search

Field Value
Platform PortSwigger Web Security Academy
Vulnerability DOM-Based Cross-Site Scripting (XSS)
Difficulty Apprentice
Source location.search (URL query string)
Sink document.write()
Goal Break out of the attribute context and execute JavaScript

What is the DOM?

DOM stands for Document Object Model — it is the tree structure that represents an HTML document in memory. When a browser loads a page, it parses the HTML and builds this tree. Every HTML tag becomes a node:

html
├── head
│   └── title
└── body
    ├── h1
    ├── p
    └── div
        └── img

JavaScript can read and modify this tree at runtime using the DOM API — adding, removing, or changing elements and attributes without making a new HTTP request.

DOM-based XSS occurs when JavaScript reads data from an attacker-controlled source (like location.search) and writes it back to the DOM using a dangerous function (like document.write()). The vulnerability is entirely in the client-side code — the server may never see the malicious payload in a dangerous form.


Phase 1 — Reconnaissance

We find a blog with a search bar. Searching for any value puts it in the URL:

/?search=search
Screenshot

Phase 2 — Testing Basic Injection

<script>alert(0)</script>
Screenshot

No alert. Inspecting the HTML in DevTools revealed why — the input landed inside an img tag attribute, not as free HTML:

<img src="/resources/images/tracker.gif?searchTerms=&lt;script&gt;alert(0)&lt;/script&gt;">

The < and > were HTML-encoded inside the attribute value — the script tag is treated as literal text. The payload is trapped inside a string. The server returned a safe response; the vulnerability lies elsewhere.


Phase 3 — Identifying the Sink

The application is running JavaScript like this:

document.write('<img src="/resources/images/tracker.gif?searchTerms=' + location.search + '">');

Our input is concatenated directly into the src attribute of an img tag via document.write(). The source is location.search — the URL query string. The sink is document.write() — a function that writes raw HTML to the page.

To execute JavaScript we need to break out of the attribute context first, then inject script as free HTML.


Phase 4 — Breaking Out of the Attribute Context

"><script>alert("hola")</script>
  • " — closes the src attribute value
  • > — closes the <img> tag
  • <script>alert("hola")</script> — payload injected as free HTML
Screenshot

The resulting DOM:

<img src="/resources/images/tracker.gif?searchTerms=">
<script>alert("hola")</script>
Screenshot

Alert fired. Lab solved.