Framework Protections & Modern XSS

Chapter 9
Framework Protections & Modern XSS
Auto-escaping, the escape hatches that reopen XSS, and Trusted Types

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:

// React — {value} is auto-escaped; a payload renders as text function Hello({ name }) { return <h1>Hello, {name}</h1>; // name="<script>…" -> shown as text } // Vue — {{ value }} auto-escapes; Angular — {{ value }} auto-escapes

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:

FrameworkEscape hatchRisk
ReactdangerouslySetInnerHTMLinjects raw HTML — name is the warning
Vuev-htmlsets innerHTML, no escaping
Angular[innerHTML] + bypassSecurityTrust…bypasses Angular's sanitizer
Svelte{@html ...}raw HTML insertion
// the dangerous pattern — raw untrusted HTML into the DOM <div dangerouslySetInnerHTML={{ __html: userComment }} /> // XSS if unsanitized // the fix — sanitize first (Chapter 7) <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userComment) }} /> // safe
Auditing a modern app for XSS = grep for the escape hatches
Because ordinary interpolation is safe, XSS review in a React/Vue/Angular codebase largely reduces to finding the explicit bypasses: search for 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/src can be javascript:. 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.

Content-Security-Policy: require-trusted-types-for 'script' // now: element.innerHTML = userString -> THROWS (string not allowed) // must pass through a policy that sanitizes and returns a TrustedHTML object const policy = trustedTypes.createPolicy("safe", { createHTML: s => DOMPurify.sanitize(s) }); element.innerHTML = policy.createHTML(userString); // allowed & sanitized
Why Trusted Types is powerful: it makes the unsafe path impossible, not just discouraged
Conventional DOM-XSS advice is "remember to sanitize before 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

Exercise 1

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.

📄 View solution
Exercise 2

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 solution
Exercise 3

Explain 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 solution

Chapter 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)