XSS (Cross-Site Scripting)
One-line relationship: XSS is attacker-controlled data that gets interpreted as attacker-controlled code — the highest-frequency web vulnerability class, and the reason CSP exists as a backstop.
What it is
Any time user-controlled input reaches the DOM as markup rather than as text, the browser will parse whatever that input contains — including <script> tags and event-handler attributes like onerror. If an attacker can get their string into that path, their JavaScript now runs with the full privileges of your page: it can read cookies (unless HttpOnly), read any bearer token sitting in memory, make authenticated API calls as the logged-in user, or rewrite the DOM to phish credentials in place.
Three shapes, same root cause:
- Stored — the payload is saved server-side (a comment, a profile field) and served to every later visitor.
- Reflected — the payload rides in the request (a query parameter) and is echoed straight back into the response.
- DOM-based — the payload never touches the server at all; client-side JavaScript reads something attacker-controlled (
location.hash,document.referrer) and writes it into the DOM unsafely.
Try it — escaping vs raw insertion
Imagine this is a comment box on a page that echoes back whatever a user typed. Nothing here is ever actually executed — the "unsafe" panel is a simulation of what raw HTML insertion would do, rendered as inert text.
Unsafe — page does el.innerHTML = comment (simulated, not executed here)
Safe — page does el.textContent = comment (or a templating engine that HTML-encodes)
The lesson: the payload above is identical in both panels — the difference is entirely in how the page inserts it into the DOM.innerHTML(or any raw HTML sink) parses attacker-controlled strings as markup, so a crafted tag's event handler executes as your page's own JavaScript.textContent, an auto-escaping template, or output encoding turns the same string into inert display text. Content-Security-Policy (next page) is the backstop for whatever escaping misses.
The defense stack
- Output encoding — the only real fix. Anything user-controlled that’s rendered as HTML must be HTML-entity-encoded (
<→<) before insertion; anything inserted as an attribute, URL, or JS string needs the encoding appropriate to that context. Modern frameworks (React, Vue) auto-escape by default — the danger is almost always an explicit opt-out (dangerouslySetInnerHTML,v-html, rawinnerHTML =). - Content-Security-Policy — the backstop for whatever escaping misses: a tight policy with no
'unsafe-inline'means even a successfully injected<script>tag simply won’t execute. See CSP. - Trusted Types — a browser API that locks down the dangerous DOM sinks (
innerHTML,document.write) so they only accept values that passed through an explicit sanitizer, turning a whole class of DOM-XSS bugs into build-time type errors.
What to take to the client
XSS is the highest-volume finding in almost every web pentest, and it’s also the vulnerability that makes every other client-side control (cookies, tokens in memory, even a WebAuthn session) moot — a successful injection runs as your page, with your page’s access. Escaping is non-negotiable at every output boundary; CSP is what catches the boundary someone inevitably misses.