How to enable Trusted Types with CSP to stop DOM XSS
CentralCSP Team ·
Last update:
Trusted Types is the part of a Content Security Policy (CSP) that closes DOM-based cross-site scripting (XSS). A CSP is an HTTP response header that tells the browser which scripts may run, and Trusted Types extends that to the DOM sinks that turn a string into live markup or code: innerHTML, document.write, eval, the Function constructor, and a few dozen others. With Trusted Types enforced, those sinks reject a plain string. The only values they accept are typed objects your own policies produced, so an injected string never reaches a sink.
The short version: set require-trusted-types-for to 'script' to turn enforcement on, use the trusted-types directive to allowlist the policy names that may create those typed values, and roll it out in Report-Only first so you see every sink before you block one. The rest of this post is the how-to, including the framework patterns for Angular, React, and DOMPurify.
This builds on trusted-types-eval, a safer way to allow eval in CSP, which covers the eval sink specifically. For the inline-script and string-to-code problems Trusted Types sits next to, see why you should never use unsafe-inline in CSP and unsafe-eval and how to remove it.
What Trusted Types protects against
DOM XSS happens when client-side code passes attacker-controlled text into a sink that parses it as HTML or executes it as script. A classic case is element.innerHTML = location.hash. Server-side defenses never see it, because the dangerous assignment happens in the browser, after the page loads, from data the server may never have touched.
Trusted Types changes the rule at the sink. When enforcement is on, an injection sink no longer accepts a string at all. It accepts a TrustedHTML, TrustedScript, or TrustedScriptURL object, and the only way to make one is to run a string through a registered policy function you wrote. That gives you one auditable place where every value crossing into a sink is checked, and it makes the unsafe pattern fail loudly instead of executing.
Turn enforcement on with require-trusted-types-for
One directive switches Trusted Types on. The require-trusted-types-for directive takes the single token value 'script', which tells the browser to enforce Trusted Types at the script-related DOM sinks:
Content-Security-Policy: require-trusted-types-for 'script'With that header present, assigning a plain string to innerHTML (or any covered sink) throws a TypeError and emits a violation report. Nothing else runs differently. This directive has no fallback to default-src, so it only applies when you set it explicitly.
Allowlist your policies with the trusted-types directive
Enforcement on its own would block your own code too, because your code also writes to those sinks. The trusted-types directive names which Trusted Types policies the page is allowed to create. A policy is a small object with createHTML, createScript, or createScriptURL functions that vet a string and return the typed value.
List the policy names you intend to register:
Content-Security-Policy: trusted-types myPolicyA few tokens are worth knowing:
'none'forbids creating any policy at all, the strictest setting, useful once you have removed every direct sink write.*allows any unique policy name (less strict, convenient during rollout).'allow-duplicates'permits registering the same policy name more than once, which some bundlers and micro-frontends need.defaultis the reserved name for the default policy. If you register a policy called"default", the browser runs itscreate*function automatically on any plain string passed to a sink, which is how you retrofit Trusted Types onto code you cannot edit. Use it deliberately, because a weak default policy widens the surface back out.
Combine both directives in one policy, one directive per line for readability:
Content-Security-Policy:
require-trusted-types-for 'script';
trusted-types myPolicyRegister a policy in your code
A policy is where you put the actual sanitization. Create it once, then route every sink write through it. Here is a minimal policy that sanitizes HTML before it becomes a TrustedHTML:
// Register a named policy. The name must be in the trusted-types directive.
const policy = window.trustedTypes.createPolicy("myPolicy", {
createHTML: (input) => {
// Do the real sanitization here. Returning input untouched is not safe;
// the policy is only as strong as the check inside it.
return sanitize(input);
},
});
// A plain string is now rejected at the sink.
element.innerHTML = userInput; // throws TypeError under enforcement
// A TrustedHTML from your policy is accepted.
element.innerHTML = policy.createHTML(userInput); // runsThe value is the chokepoint, not the assignment. Every string that becomes markup passes through createHTML, so you have one function to review, test, and harden, instead of auditing every innerHTML in the codebase.
Roll it out in Report-Only first
Switching enforcement on for a live site will break anything that writes a raw string to a covered sink, which on most apps is a lot of places you do not know about yet. Do it in stages and watch reports before you enforce, the same Report-Only-first approach as any CSP change covered in how to build a strong CSP, step by step.
Ship the directives on the Content-Security-Policy-Report-Only header. The browser blocks nothing; it sends a violation report for every sink write that enforcement would have rejected.
Content-Security-Policy-Report-Only:
require-trusted-types-for 'script';
trusted-types myPolicy;
report-to csp-endpointWire the endpoint with the Reporting-Endpoints header:
Reporting-Endpoints: csp-endpoint="https://<Endpoint-ID>.report.centralcsp.com"Trusted Types violations do not introduce a new report type. They arrive as standard CSP violation reports, so they land at the same endpoint and read with the same effectiveDirective, sample, and sourceFile fields as any other blocked script. Collect them, fix each sink (route it through a policy or remove it), and only flip to the enforcing Content-Security-Policy header once Report-Only is quiet.
CentralCSP ingests those Report-Only reports and groups them by page and sink, so you can see which parts of the app still write raw strings before you enforce. You can also check a draft policy for weak spots with the free CSP evaluator before you ship it.
Framework patterns
Most apps do not call sinks directly; a framework does it for them. The Trusted Types work is then mostly about how that framework gets its create* function.
Angular
Angular has built-in Trusted Types support. Its DomSanitizer already runs through a Trusted Types policy, named angular for the framework and angular#bundler for the build output, so an Angular app can run under enforcement once you allowlist those policy names in the directive. List the ones your build uses:
Content-Security-Policy:
require-trusted-types-for 'script';
trusted-types angular angular#bundlerAngular registers angular and angular#bundler as its base policy names. Depending on your build, it may also register angular#unsafe-bypass, angular#unsafe-jit, or angular#unsafe-upgrade, so watch your Report-Only reports and allowlist the ones your build actually uses. If you call sinks yourself outside Angular's APIs, register your own additional policy and add its name to the list.
React with DOMPurify
React escapes text by default, but dangerouslySetInnerHTML writes straight to the DOM, so it is the sink to guard. DOMPurify sanitizes HTML and can return a Trusted Types value directly with the RETURN_TRUSTED_TYPE option, which means the sanitized output is already a TrustedHTML your policy produced:
import DOMPurify from "dompurify";
// DOMPurify creates a Trusted Types policy named "dompurify" internally.
const clean = DOMPurify.sanitize(dirtyHtml, { RETURN_TRUSTED_TYPE: true });
// clean is a TrustedHTML, accepted by the sink under enforcement.
<div dangerouslySetInnerHTML={{ __html: clean }} />;Allowlist the policy DOMPurify registers (dompurify) plus any of your own:
Content-Security-Policy:
require-trusted-types-for 'script';
trusted-types dompurify myPolicyThis pattern works for any framework that exposes a raw-HTML sink: sanitize with DOMPurify in RETURN_TRUSTED_TYPE mode, pass the typed result to the sink, and allowlist dompurify.
Browser support today
Trusted Types is strongest in Chromium, which has shipped require-trusted-types-for and trusted-types for years. Firefox added support more recently, and the directives recently became available across current Chrome, Firefox, and Safari. Support outside Chromium was historically limited, so treat broad cross-browser enforcement as recent.
The behavior degrades safely. A browser that does not enforce Trusted Types ignores the directives and runs the page as before, so shipping them does not break older clients; you simply do not get the protection there. That makes it safe to enable now and gain the protection wherever the browser supports it.
Frequently asked questions
How do I enable Trusted Types?
Add require-trusted-types-for 'script' to your CSP to turn enforcement on, and add a trusted-types directive listing the policy names your code creates. Route every DOM sink write through one of those policies, and roll it out on the Content-Security-Policy-Report-Only header first so you catch every sink before you block one.
What does require-trusted-types-for do?
It tells the browser to enforce Trusted Types at script-related DOM sinks like innerHTML, document.write, and eval. Its only value is the token 'script'. With it set, those sinks reject a plain string and accept only a typed object produced by one of your registered policies.
What is the default Trusted Types policy?
A policy registered with the reserved name "default". The browser runs its create* function automatically on any plain string passed to a sink, which retrofits Trusted Types onto code you cannot change. It is convenient but widens the surface, so keep the default policy's checks strict.
Can I use Trusted Types with React?
Yes. Guard dangerouslySetInnerHTML by sanitizing with DOMPurify in RETURN_TRUSTED_TYPE mode, which returns a TrustedHTML the sink accepts under enforcement, then allowlist the dompurify policy name in the trusted-types directive.
The takeaway
Enabling Trusted Types is two directives and one habit: require-trusted-types-for 'script' turns enforcement on, trusted-types allowlists the policies that may create typed values, and every sink write goes through a policy you can audit. Roll it out in Report-Only, fix the sinks the reports surface, then enforce. Frameworks do most of the work: Angular ships its own policies, and DOMPurify hands React a TrustedHTML directly.
If you want the rollout instrumented, start free with CentralCSP, point a Report-Only header at it, and watch which sinks still need a policy before you enforce.
Sources
- W3C, Trusted Types specification
- MDN, Trusted Types API
- Angular, security and Trusted Types
- DOMPurify, source and options