UnveilTech

UnveilScan Blog

← All articles

Try UnveilScan free

Trusted Types: structurally killing DOM XSS

Posted 2026-04-29 · 8 min read · WEBCSP

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:

  1. 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.
  2. Phase 2 — refactor the easy ones. Most innerHTML = "
    " + escape(s) + "
    "
    patterns convert to textContent, createElement + appendChild, or a tiny templating function. Most calls don't need HTML — they need text.
  3. 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.
  4. Phase 4 — third-party JS. Your analytics, ads, payments embed call document.write or set inline event handlers. They need their own policy, allowed via trusted-types name-of-third-party 'allow-duplicates'.
  5. Phase 5 — flip to enforce. Drop the -Report-Only suffix. 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

Browser support in 2026

BrowserSupportNotes
Chrome / Edge / BraveFullSince 83
FirefoxFullSince 139 (2025-Q1)
SafariPartial → FullShipping 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