Trusted Types: structurally killing DOM XSS
Most XSS in 2026 is DOM XSS, not reflected XSS. The pattern: untrusted input flows
through innerHTML or document.write or
eval or a setAttribute("href", ...) call that ends up as
javascript:. CSP script-src doesn't catch these because
the bad data flow happens client-side without injecting a new <script>.
Trusted Types is the structural fix: the browser refuses to accept strings into the DOM's dangerous sinks unless the strings have been "minted" by an explicit policy defined in your code. Originally Google-only, now in Chrome (since 83), Edge, and Firefox (139, 2025-Q1).
The directive
Content-Security-Policy: require-trusted-types-for 'script';
trusted-types app-policy 'allow-duplicates';
With require-trusted-types-for 'script', the browser throws on any string
passed to: innerHTML, outerHTML, insertAdjacentHTML,
document.write, document.writeln, setAttribute
for an event-handler-named attribute, javascript: URLs, and
eval/Function constructors.
To pass a string to innerHTML after enforcement, you must mint it via a
policy:
const policy = trustedTypes.createPolicy('app-policy', {
createHTML: (input) => DOMPurify.sanitize(input)
});
element.innerHTML = policy.createHTML(userInput);
The policy is a centralized choke point. If a vulnerability slips into the codebase
that calls el.innerHTML = userInput directly, it throws. Sanitization
happens in one place that's easier to audit.
The migration is the work
Most apps have hundreds of innerHTML calls. Migration is the dominant
cost. The standard playbook:
- Phase 1 — report-only. Deploy
Content-Security-Policy-Report-Only: require-trusted-types-for 'script'; report-uri /csp-report. Browser logs violations but doesn't block. Run for 2-4 weeks. Collect the inventory of dangerous calls. - Phase 2 — refactor the easy ones. Most
innerHTML = "patterns convert to" + escape(s) + ""textContent,createElement + appendChild, or a tiny templating function. Most calls don't need HTML — they need text. - Phase 3 — DOMPurify the rest. Anything that legitimately needs to render user-supplied HTML (rich text editors, comment systems) goes through DOMPurify wrapped in a single named policy.
- Phase 4 — third-party JS. Your analytics, ads, payments embed call
document.writeor set inline event handlers. They need their own policy, allowed viatrusted-types name-of-third-party 'allow-duplicates'. - Phase 5 — flip to enforce. Drop the
-Report-Onlysuffix. Watch for an hour. Roll back if anything new breaks.
Third-party JS is the obstacle
Google Analytics, Stripe.js, Intercom, Hotjar, Sentry, all violate Trusted Types in one way or another. Some have updated their libraries; many haven't. The standard escape hatch:
trusted-types app-policy ga-policy stripe-policy 'allow-duplicates'
Each named third party can register its own policy with the same name as itself. 'allow-duplicates' allows the same name to register multiple times (some bundlers create multiple instances of the same lib).
A pure 'none' policy is the gold standard but unrealistic for most apps. The intermediate state — where your code is forced through a sanitization policy and third parties are explicitly allowlisted — still kills most DOM XSS.
What it does NOT solve
- Mutation XSS. Sanitizers like DOMPurify can be bypassed if the browser's HTML parser interprets the sanitized output differently from the sanitizer. Periodic CVEs in DOMPurify itself.
- Stored XSS via API responses with the wrong Content-Type. A JSON API that returns text/html instead leads to direct rendering. Not a Trusted Types problem; an output-encoding problem.
- postMessage handler abuse. Cross-origin postMessage with an over-permissive listener that does
document.body.innerHTML = ev.data. Trusted Types catches this exact form, but the broader class of cross-window data flows is its own audit.
Browser support in 2026
| Browser | Support | Notes |
|---|---|---|
| Chrome / Edge / Brave | Full | Since 83 |
| Firefox | Full | Since 139 (2025-Q1) |
| Safari | Partial → Full | Shipping in 18.x; full enforcement in stable as of 2026 |
Browsers without support ignore the directive — your Trusted Types policy doesn't break them, but they also don't get the protection. Plan for "real protection on Chromium, defense in depth elsewhere."
Detection from outside
Our headers checker reports the CSP. The csp checker grades
its maturity. Trusted Types presence is currently informational in our scoring —
absence isn't a defect (most sites haven't migrated), presence is a strong positive
signal of mature security engineering.
Grade your CSP maturity
Free Basic scan reports the headers. Extended grades CSP maturity (unsafe-inline, wildcards, nonces, frame-ancestors).
Run a scan