JavaScript Trusted Types Table

PieceWhat it doesField note
Trusted Types policyGate innerHTML sinksDOM sinks REJECT plain strings unless a policy created them
trustedTypes.createPolicyThe named factorycreateHTML/createScript - your sanitization, enforced
require-trusted-types-forThe CSP directiveEnforce platform-wide - violations become console errors
default policyCatch-all transformOne policy named 'default' sanitizes everything unmatched
Sink listThe attack surfaceinnerHTML, outerHTML, eval, src=javascript: - string-eating APIs
Audit by errorsEnforce in report modeContent-Security-Policy-Report-Only finds every sink first
vs sanitizersTypes enforce, libs cleanDOMPurify cleans content; TT makes bypassing impossible
Big-framework readyReact/Vue emit safeFrameworks' string sinks are the migration work - plugins exist
Reference: the MDN Trusted Types reference. Trusted Types flips XSS defense from convention to TYPE SYSTEM: with the CSP directive require-trusted-types-for 'script', every DOM sink that eats strings (innerHTML, eval, script src) REJECTS plain strings - only values produced by a named TrustedTypes policy pass, and the policy is where your sanitization lives. Bottom line: sanitizers depend on every developer remembering to call them; Trusted Types makes the unsafe call a console error at runtime - enforce in report-only mode first (the audit finds every sink), then flip. Related tools: security headers table (the CSP that enforces it), DOM manipulation table (the sinks being gated), and CORS table (the other platform enforcement story).

Trusted Types flips XSS defense from convention to TYPE SYSTEM: with the CSP directive require-trusted-types-for 'script', every DOM sink that eats strings (innerHTML, eval, script src) REJECTS plain strings - only values produced by a named TrustedTypes policy pass.

Bottom line: sanitizers depend on every developer remembering to call them on every assignment; Trusted Types makes the unsafe call a console error AT RUNTIME - the bypass becomes impossible rather than discouraged. The migration is audit-first: enforce in report-only mode, collect every sink violation, fix or policy them, then flip.

The honest part: a policy is a gate, not a sanitizer - what createHTML does with the string is your code (usually calling DOMPurify inside). And large codebases have hundreds of sink assignments; the report-only phase exists because flipping enforcement on day one breaks all of them at once.

How to use

  1. Audit before enforcing: Content-Security-Policy-Report-Only: require-trusted-types-for 'script' with a report-uri - the violations list IS the migration backlog.
  2. Create one sanitize policy: trustedTypes.createPolicy('app', { createHTML: s => DOMPurify.sanitize(s) }) - every allowed innerHTML assignment routes through it.
  3. Fix sinks in order: const allowed = trustedTypes.createHTML('<b>ok</b>'); el.innerHTML = allowed - or better, replace innerHTML sinks with textContent and builders.

Frequently asked questions

What exactly does the require-trusted-types-for directive change?

The type contract of DOM sinks. Historically, innerHTML = anythingString compiled and ran - the XSS hole, guarded only by developer discipline (sanitize at every assignment site, forever). With require-trusted-types-for 'script' enforced, those sinks demand a TrustedHTML object - a value that can ONLY be created by a policy you registered. A raw string now throws a TypeError at the assignment. The security property: the dangerous operation is unreachable without passing through your policy, so sanitizer coverage stops being a per-developer promise and becomes a platform-enforced invariant. eval and script URLs fall under the same regime ('script' sinks); HTML sinks via the default policy or explicit types.

How does the default policy work and when should it exist?

One policy named 'default' catches everything no named policy handled: any sink assignment with a raw string routes through default policy's createHTML/createScript/createScriptURL, which may sanitize-and-allow or throw. It is the pragmatic middle path: instead of auditing and wrapping every one of hundreds of assignments, the default policy sanitizes them all centrally (DOMPurify inside), and teams migrate hot spots to explicit named policies over time. The trade: a default policy sanitizes by CONVENTION again - a bug in it reopens the hole - which is why the end state for high-security code is explicit named policies and no default, while the migration state for everyone else is default-plus-DOMPurify.

Why is report-only mode the mandatory first phase?

Because enforcement without an audit is an outage generator. Flipping require-trusted-types-for on a large codebase throws at EVERY unsanitized sink assignment - features break in production the moment enforcement lands. Report-only mode runs the same detection but only REPORTS violations (to a report-uri or ReportingObserver), producing the complete inventory of sinks: which files, which lines, how often. That list is the migration backlog, sorted by frequency and risk. Teams typically run report-only for weeks, fix or explicitly allowlist sinks, and flip enforcement only when reports hit zero - the same audit-first discipline CSP itself uses.

How do Trusted Types and sanitizers like DOMPurify relate?

Enforcement versus implementation - they need each other. DOMPurify cleans HTML: given a string, it returns safe HTML. But nothing FORCES its use - any developer can assign a raw string to innerHTML, and coverage depends on code review. Trusted Types adds the force: sinks accept only policy-created values, and the policy's createHTML is where DOMPurify lives - so the sanitizer becomes the only path to every HTML sink. The division: TT answers 'is this value ALLOWED to reach the sink' (type-level), DOMPurify answers 'is this value actually safe' (content-level). A policy that just returns its input would satisfy the type system while reintroducing the hole - the gate is only as good as the sanitizer inside it, which is why the pairing is the standard architecture.

Related tools