Skip to main content

Trusted Types API

The Trusted Types API prevents DOM-based XSS by enforcing that dangerous DOM sinks (innerHTML, eval(), document.write(), script.src, etc.) only accept typed objects instead of raw strings. When enforced via CSP, any attempt to pass a plain string to a dangerous sink throws a TypeError.

Browser support: Trusted Types and related CSP directives ship in Chrome/Edge 83+, Safari 26+, and Firefox 148+. Older browsers ignore the directives, so Trusted Types should still be paired with strong sanitization and other XSS defenses.

The problem​

DOM XSS happens when untrusted data reaches a dangerous DOM sink:

// Any of these accept arbitrary strings β€” a single unsanitized value means XSS
element.innerHTML = userInput;
document.write(userInput);
element.insertAdjacentHTML("beforeend", userInput);
scriptEl.src = userInput;
eval(userInput);
setTimeout(userInput, 0);

Trusted Types moves the security boundary from "hope developers remember to sanitize" to "the browser enforces that only policy-wrapped values reach sinks."

Core concepts​

Trusted Types introduces three wrapper types, each accepted by specific sinks:

TypeRepresentsSink examples
TrustedHTMLSafe HTML stringinnerHTML, outerHTML, document.write(), insertAdjacentHTML()
TrustedScriptSafe script bodyeval(), setTimeout(string), setInterval(string)
TrustedScriptURLSafe script URLscript.src, new Worker(url), dynamic import()

A policy is a named factory that defines how to transform raw strings into trusted type objects. All sanitization/validation logic is centralized in policies.

Example β€” creating a policy​

const escapePolicy = trustedTypes.createPolicy("my-escape-policy", {
createHTML: (input) => input.replaceAll("&", "&amp;").replaceAll("<", "&lt;"),
});

// Returns a TrustedHTML object β€” accepted by innerHTML
element.innerHTML = escapePolicy.createHTML(untrustedInput);

Example β€” the default policy​

A policy named 'default' acts as a fallback. When a raw string reaches a dangerous sink, the browser calls the default policy's createHTML/createScript/createScriptURL and uses that converted value. This is useful for gradual migration of legacy code:

trustedTypes.createPolicy("default", {
createHTML: (input, type, sink) => {
console.warn("Unguarded sink usage:", type, sink);
// Sanitize as a safety net
return DOMPurify.sanitize(input);
},
});

// Legacy code still works β€” goes through the default policy
element.innerHTML = untrustedString;

CSP configuration​

Two CSP directives control Trusted Types:

Content-Security-Policy: require-trusted-types-for 'script'; trusted-types my-policy dompurify default
  • require-trusted-types-for 'script' β€” enforces that DOM sinks only accept trusted type objects
  • trusted-types <policy-list> β€” restricts which policy names can be created (prevents attackers from creating arbitrary policies)

Start with report-only mode to identify violations without breaking the page:

Content-Security-Policy-Report-Only: require-trusted-types-for 'script'; trusted-types my-policy default; report-uri /csp-report

Integration with DOMPurify​

DOMPurify (v2.2+) has built-in Trusted Types support. When RETURN_TRUSTED_TYPE: true is set and the browser supports Trusted Types, DOMPurify.sanitize() returns a TrustedHTML object:

import DOMPurify from "dompurify";

// DOMPurify returns a TrustedHTML object in supporting browsers
const clean = DOMPurify.sanitize(dirty, { RETURN_TRUSTED_TYPE: true });
element.innerHTML = clean; // Accepted β€” it's a TrustedHTML, not a string

For full-application enforcement, wrap DOMPurify in the default policy:

import DOMPurify from "dompurify";

trustedTypes.createPolicy("default", {
createHTML: (input) => DOMPurify.sanitize(input, { RETURN_TRUSTED_TYPE: true }),
createScriptURL: (input) => {
const url = new URL(input, document.baseURI);
if (url.origin === location.origin) return input;
throw new TypeError("Blocked untrusted script URL: " + input);
},
});

This gives you Trusted Types enforcement with DOMPurify as the sanitization engine β€” combining runtime type enforcement (trusted types block accidental raw string usage at dangerous sinks) with runtime sanitization (DOMPurify strips dangerous content). See also the HTML Sanitizer API for the upcoming native browser sanitizer.

Migration strategy​

  1. Deploy report-only CSP β€” add Content-Security-Policy-Report-Only: require-trusted-types-for 'script' and monitor violations
  2. Identify all sink usages β€” review reports in DevTools console or your reporting endpoint
  3. Create focused policies β€” one per concern (e.g., dompurify for HTML, url-validator for script URLs)
  4. Wrap sinks β€” replace raw string assignments with policy calls
  5. Add a default policy as a safety net during migration
  6. Switch to enforcement β€” change Report-Only to enforcing Content-Security-Policy
  7. Restrict policy names β€” add trusted-types <list> to lock down which policies exist

Limitations​

  • Older-browser gaps β€” modern Chrome/Edge, Firefox, and Safari enforce Trusted Types, but older browsers ignore the directives; treat Trusted Types as defense-in-depth, not a standalone solution
  • Third-party scripts β€” libraries using innerHTML or eval() break under enforcement unless they support Trusted Types or you wrap them in a default policy
  • No enforcement polyfill β€” polyfills like trusted-types provide the API surface but cannot enforce sink restrictions in non-supporting browsers
  • Migration effort β€” large codebases with many innerHTML usages require systematic wrapping; the default policy eases this but can mask issues if overused
  • Framework support varies β€” Angular's DomSanitizer is Trusted Types–compatible; React does not have native integration (dangerouslySetInnerHTML is the main relevant sink)

See also​