05. Cross-Site Scripting
XSS lets an attacker run their JavaScript in someone else's browser, on a site that person trusts.
Why It Matters
JavaScript running on bank.com can do anything the logged-in user can: read the page, make requests as them, change settings, or steal non-HttpOnly cookies. alert() is just the proof that you can run code.
How It Happens
The app puts your input into the page without encoding it:
<!-- template -->
<p>Results for: {{ query }}</p>
<!-- you search for: <b>hi</b> -->
<p>Results for: <b>hi</b></p> ← rendered as bold, so HTML injection works
If HTML works, try making it run JavaScript.
Three Flavors
| Type | Where the payload lives | Example |
|---|---|---|
| Reflected | In the request (usually the URL). Victim must click your link. | /search?q=<payload> |
| Stored | Saved by the app and shown to everyone who views it | Comments, profiles, guestbooks |
| DOM-based | JavaScript on the page reads input and writes it into the DOM | el.innerHTML = location.hash |
Payloads
<!-- the classic -->
<script>alert(1)</script>
<!-- event handlers: work even where <script> doesn't -->
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<details open ontoggle=alert(1)>
<!-- breaking out of an attribute: value="INPUT" -->
"><img src=x onerror=alert(1)>
" autofocus onfocus="alert(1)
Gotcha: innerHTML
If JavaScript inserts your input with
innerHTML, <script> tags will not run.
Use an event handler payload like <img src=x onerror=alert(1)> instead.
Sources & Sinks
For DOM XSS, trace where data comes from (source) to where it's used dangerously (sink):
Sources
location.searchlocation.hashdocument.cookielocalStorage
Sinks
innerHTML/outerHTMLdocument.write()eval()element.src/.href
The Fix
- Output encoding: turn
<into<. Modern template engines do this by default. - Use
textContentinstead ofinnerHTML - Content-Security-Policy header as defense in depth
- HttpOnly cookies so JavaScript can't read session tokens