Framework Protections & Modern XSS
Modern frameworks dramatically reduced XSS — not by magic, but by making output encoding the default so developers get it right without thinking. But every framework also provides escape hatches to render raw HTML, and those are where modern XSS lives. This chapter covers how the protection works, where it leaks, and the newest browser defence: Trusted Types.
Auto-Escaping: Safe by Default
React, Vue, and Angular all context-aware-encode interpolated values automatically. When you bind a variable into a template, the framework escapes it for you — the Chapter 6 discipline, built in:
This is why XSS rates fell as frameworks took over: the default path is safe, so the most common mistake (forgetting to encode) is eliminated for ordinary interpolation. The flip side — exactly like SameSite's "less, not gone" from the CSRF course — is that the escape hatches keep XSS alive.
The Escape Hatches — Where Modern XSS Lives
Each framework has an explicit "render this as raw HTML" API that bypasses auto-escaping. They're deliberately awkwardly named to signal danger:
| Framework | Escape hatch | Risk |
|---|---|---|
| React | dangerouslySetInnerHTML | injects raw HTML — name is the warning |
| Vue | v-html | sets innerHTML, no escaping |
| Angular | [innerHTML] + bypassSecurityTrust… | bypasses Angular's sanitizer |
| Svelte | {@html ...} | raw HTML insertion |
dangerouslySetInnerHTML, v-html, bypassSecurityTrustHtml, {@html}, and direct innerHTML/document.write usage. Each is a place where auto-escaping was switched off and the input must be sanitized (DOMPurify). These greppable markers make modern XSS auditing far more tractable than the old "every output everywhere" problem — but you must actually check each one.
It's Not Only innerHTML — Other Framework Sinks
Auto-escaping covers text interpolation, but some bindings remain dangerous because the value itself is a code-ish context (Chapter 3):
- URL bindings — a user-controlled
href/srccan bejavascript:. React/Angular block some schemes, but binding untrusted URLs still needs scheme validation. - Dynamic attribute/prop names and spreading untrusted objects into props can introduce handlers.
- Server-side rendering (SSR) + hydration can reintroduce injection if data is interpolated into the initial HTML or a
<script>JSON island unsafely. - Template injection — if untrusted data reaches the template compiler (e.g. server-side template engines, or
eval-like dynamic templates), it's a far worse class than DOM XSS.
DOM XSS Hasn't Gone Away
Even in framework apps, plain DOM-XSS sinks (Chapter 2) bite when developers reach outside the framework: a stray element.innerHTML = userData, document.write, eval, setTimeout("string"), or assigning untrusted data to location. The framework only protects what flows through its templating; raw DOM manipulation is on you.
Trusted Types — Killing DOM XSS at the Sink
Trusted Types is a modern browser feature (enabled via CSP) that attacks DOM XSS structurally: it makes dangerous sinks (innerHTML, document.write, etc.) refuse plain strings — they only accept special typed objects produced by a policy you define and control.
innerHTML" — which fails the moment one developer forgets. Trusted Types inverts the default: assigning a raw string to a dangerous sink simply throws an error, so the only way to use the sink is through your sanitizing policy. It turns "don't forget to sanitize" (a discipline that erodes) into "you physically cannot inject an unsanitized string" (a guarantee enforced by the browser). It also gives you a single chokepoint to audit — the policy — instead of every sink in the codebase. Adoption is growing; it's the strongest structural answer to DOM XSS.
Hands-On Exercises
Explain why <h1>Hello, {name}</h1> in React is safe even when name contains a script payload, but dangerouslySetInnerHTML={{__html: name}} is not. Give the correct safe version of the second one.
You're security-reviewing a Vue/React/Angular codebase. List the specific APIs and sinks you'd grep for, and explain why auto-escaping means XSS review concentrates on these rather than on every template binding. Note one non-innerHTML sink that auto-escaping doesn't cover.
📄 View solutionExplain how Trusted Types prevents DOM-based XSS, contrasting "remember to sanitize before innerHTML" with "the sink rejects raw strings." Why does this convert a fragile discipline into an enforced guarantee, and what becomes the single thing you audit?
📄 View solutionChapter 9 Quick Reference
- Auto-escaping — React
{x}, Vue/Angular{{ x }}context-encode by default; why framework apps have far less XSS - Escape hatches reopen it:
dangerouslySetInnerHTML(React),v-html(Vue),bypassSecurityTrust…/[innerHTML](Angular),{@html}(Svelte) - Fix the hatch by sanitizing first (DOMPurify) — never feed raw untrusted HTML to it
- Audit modern apps by grepping the hatches + raw
innerHTML/document.write/eval - Auto-escaping doesn't cover everything: URL bindings (
javascript:), SSR/hydration, prop spreading, template injection - Plain DOM XSS still bites when you bypass the framework with raw DOM APIs
- Trusted Types (via CSP
require-trusted-types-for 'script') — dangerous sinks reject raw strings; only a sanitizing policy's typed output is accepted - Trusted Types turns "remember to sanitize" into an enforced guarantee with one auditable chokepoint — the strongest structural DOM-XSS defence
- Next chapter: testing, pitfalls & a deployable hardening checklist (the course finale)