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:
| Type | Represents | Sink examples |
|---|---|---|
TrustedHTML | Safe HTML string | innerHTML, outerHTML, document.write(), insertAdjacentHTML() |
TrustedScript | Safe script body | eval(), setTimeout(string), setInterval(string) |
TrustedScriptURL | Safe script URL | script.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("&", "&").replaceAll("<", "<"),
});
// 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 objectstrusted-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β
- Deploy report-only CSP β add
Content-Security-Policy-Report-Only: require-trusted-types-for 'script'and monitor violations - Identify all sink usages β review reports in DevTools console or your reporting endpoint
- Create focused policies β one per concern (e.g.,
dompurifyfor HTML,url-validatorfor script URLs) - Wrap sinks β replace raw string assignments with policy calls
- Add a default policy as a safety net during migration
- Switch to enforcement β change
Report-Onlyto enforcingContent-Security-Policy - 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
innerHTMLoreval()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
innerHTMLusages 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 (
dangerouslySetInnerHTMLis the main relevant sink)
See alsoβ
- HTML Sanitizer API β native browser sanitization (complementary approach)
- MDN: Trusted Types API β full API reference
- web.dev: Prevent DOM XSS with Trusted Types β comprehensive guide with migration advice