Enforcing require-trusted-types-for script
Permalink to "Enforcing require-trusted-types-for script"Part of Trusted Types & DOM XSS Prevention, this page is the focused reference for the require-trusted-types-for 'script' directive: its exact syntax, the minimum policy you need to ship alongside it, the Report-Only, default-policy, and DOMPurify-backed variants, and how to verify enforcement in the browser.
Quick Reference
Permalink to "Quick Reference"| Property | Value |
|---|---|
| Directive | require-trusted-types-for 'script' |
| Companion directive | trusted-types <policy-name>… |
| Only valid argument | 'script' (quoted keyword) |
| Delivery | Content-Security-Policy header, -Report-Only header, or <meta http-equiv> |
| Guarded sinks | innerHTML, outerHTML, script.src, script.text, document.write, eval, setTimeout/setInterval string form, DOMParser.parseFromString |
| Trusted object types | TrustedHTML, TrustedScript, TrustedScriptURL |
| Policy factory | trustedTypes.createPolicy(name, rules) |
| Chromium (Chrome/Edge/Opera) | Native enforcement |
| Firefox / Safari | No native support — use the W3C polyfill |
| Violated directive in reports | require-trusted-types-for |
The directive only takes effect when a policy exists to produce trusted objects; shipping the directive without a policy blocks every guarded sink outright.
How the Directive Works
Permalink to "How the Directive Works"require-trusted-types-for 'script' flips the DOM’s injection sinks from accepting any string to accepting only a typed trusted object. Once it is active, element.innerHTML = someString throws a TypeError — the browser will only accept a TrustedHTML produced by a policy you registered with trustedTypes.createPolicy. The companion trusted-types directive names which policies are allowed to exist, so an injected script cannot mint its own sanitizer-free policy. Together they convert DOM XSS review from auditing hundreds of sink assignments into auditing a handful of policy callbacks.
The 'script' argument scopes enforcement to the sinks that can turn a string into executing code or markup: the HTML-parsing sinks (innerHTML, outerHTML, document.write, DOMParser.parseFromString), the script-loading sinks (HTMLScriptElement.src, HTMLScriptElement.text), and the code-evaluation sinks (eval, the Function constructor, and the string form of setTimeout/setInterval). Each guarded sink maps to one trusted type: HTML sinks demand TrustedHTML, URL sinks demand TrustedScriptURL, and evaluation sinks demand TrustedScript. A policy callback you did not supply leaves the matching sink category blocked outright, which is a safe default — you opt into exactly the capabilities you sanitize for.
Canonical Example
Permalink to "Canonical Example"Ship the policy in the page and the directive in the response header. This is the minimum working enforcing setup: a policy named app-ui that sanitizes HTML, and a CSP that both enables enforcement and allow-lists that one policy name.
// app.js — loads before any code that writes to a DOM sink
if (window.trustedTypes && window.trustedTypes.createPolicy) {
window.trustedTypes.createPolicy('app-ui', {
createHTML: (input) => DOMPurify.sanitize(input),
});
}
Content-Security-Policy:
require-trusted-types-for 'script';
trusted-types app-ui;
report-to tt-violations
With both in place, panel.innerHTML = policy.createHTML(markup) succeeds while a bare panel.innerHTML = markup throws. The feature-detect guard around createPolicy matters: in a browser without native support and without the polyfill, window.trustedTypes is undefined, so calling createPolicy unguarded would itself throw and take down page load. Guarding it means the same bundle runs everywhere and simply falls back to no enforcement where the platform provides none.
Because this directive governs DOM sinks only — not the bytes a <script src> fetches — pair it with an integrity attribute so the fetched code is also verified, as detailed in Trusted Types & DOM XSS Prevention.
Variant Examples
Permalink to "Variant Examples"Report-Only rollout
Permalink to "Report-Only rollout"Deploy this first. The browser records every violation to your endpoint without breaking the page, letting you inventory unrouted sinks before enforcing:
Content-Security-Policy-Report-Only:
require-trusted-types-for 'script';
trusted-types 'none';
report-to tt-violations
trusted-types 'none' here surfaces every policy the page tries to create as a report, giving you a complete map before you write the allow-list.
Default policy for third-party sinks
Permalink to "Default policy for third-party sinks"When code you cannot edit writes to a sink, the reserved default policy intercepts it. Keep it sanitizing and logged, and remember to add default to the allow-list:
window.trustedTypes.createPolicy('default', {
createHTML: (input, _type, sink) => {
reportUnroutedSink(sink); // find the caller so you can fix it
return DOMPurify.sanitize(input);
},
createScriptURL: () => {
throw new TypeError('Unrouted script URL blocked');
},
});
Content-Security-Policy:
require-trusted-types-for 'script';
trusted-types app-ui default;
report-to tt-violations
DOMPurify-backed policy
Permalink to "DOMPurify-backed policy"DOMPurify can return a TrustedHTML directly, so its internal policy does the object creation for you — allow-list the name dompurify:
import DOMPurify from 'dompurify';
// Returns a TrustedHTML — assignable straight to a sink under enforcement.
commentBody.innerHTML = DOMPurify.sanitize(dirty, { RETURN_TRUSTED_TYPE: true });
Content-Security-Policy:
require-trusted-types-for 'script'; trusted-types dompurify
Delivery via meta tag
Permalink to "Delivery via meta tag"Where you cannot set response headers, a <meta> tag works for the enforcing (not Report-Only) policy — it must appear before any script that touches a sink:
<meta http-equiv="Content-Security-Policy"
content="require-trusted-types-for 'script'; trusted-types app-ui">
Gotchas and Edge Cases
Permalink to "Gotchas and Edge Cases"- The argument must be the quoted keyword
'script'. Writingrequire-trusted-types-for script(unquoted) is invalid and the directive is ignored, silently leaving you unprotected. There is no'style'value — the directive is script-scoped only. - Shipping the directive without a policy blocks everything. With enforcement on and no
createPolicycall, every sink assignment throws because no policy exists to produce a trusted object. Deploy the policy in the same release as the directive, not after. - Firefox and Safari need the polyfill. Native enforcement is Chromium-only as of 2026. In Firefox and Safari,
window.trustedTypesisundefinedand your feature-detect skips policy creation — so the page runs unprotected there unless you load the W3C polyfill before your app script. - Every
innerHTMLsink breaks until it is routed. Turning on enforcement without first inventorying sinks in Report-Only mode will throw at runtime the moment untested code writes to a sink. Always run a Report-Only cycle first. - A related SRI pitfall — omitting
crossorigin. When you mint aTrustedScriptURLfor an injected<script>and add anintegrityattribute, thecrossorigin="anonymous"attribute is still mandatory for cross-origin sources. Without it the browser issues a no-CORS opaque fetch it cannot hash, and the script is blocked before Trusted Types or the integrity check are even relevant. Trusted Types validate the URL string; they do not exempt you from the CORS requirement that SRI depends on. - A meta-tag policy cannot run in Report-Only mode. The
Content-Security-Policy-Report-Onlybehavior is only available through a response header. A<meta http-equiv>tag is always enforcing, so you cannot use it for the inventory phase — reserve it for the final enforcing rollout where headers are not an option. 'allow-duplicates'is not free. Bundlers that initialize twice can trip the “policy already exists” error, and adding'allow-duplicates'totrusted-typessilences it — but it also lets an injected script recreate a policy name you already use. Prefer a module-level singleton guard aroundcreatePolicyand reach for'allow-duplicates'only when a build constraint genuinely forces it.
Verification Steps
Permalink to "Verification Steps"1. Confirm the directive is delivered
Permalink to "1. Confirm the directive is delivered"Check the response header (or meta tag) is present and well-formed:
curl -sI https://example.com/ | grep -i content-security-policy
Expected output includes the directive verbatim:
content-security-policy: require-trusted-types-for 'script'; trusted-types app-ui; report-to tt-violations
2. Trigger a violation in DevTools
Permalink to "2. Trigger a violation in DevTools"In the browser console on the enforcing page, assign a raw string to a sink:
document.body.innerHTML = '<b>test</b>';
Expected result — the assignment throws and the console shows the enforcement error:
Uncaught TypeError: Failed to set the 'innerHTML' property on 'Element':
This document requires 'TrustedHTML' assignment.
3. Confirm a report is emitted
Permalink to "3. Confirm a report is emitted"A SecurityPolicyViolationEvent fires for the same violation. Listen for it to confirm your telemetry path works:
document.addEventListener('securitypolicyviolation', (e) => {
console.log(e.violatedDirective, e.sample);
});
// → "require-trusted-types-for" "innerHTML|<b>test</b>"
The violatedDirective reads require-trusted-types-for and sample names the sink and a truncated payload — the same fields your report-to endpoint receives as JSON in production.
4. Gate CI on the header staying present
Permalink to "4. Gate CI on the header staying present"A one-line regression — a proxy stripping the header, a template edit dropping the directive — silently disables all enforcement. Add a smoke check to your pipeline that fails the build if the directive is missing from the deployed response:
#!/usr/bin/env bash
# scripts/check-trusted-types.sh — run against a preview deploy URL
set -euo pipefail
URL="${1:?Usage: check-trusted-types.sh <url>}"
HEADER=$(curl -sfI "$URL" | tr -d '\r' | grep -i '^content-security-policy:')
if ! grep -q "require-trusted-types-for 'script'" <<<"$HEADER"; then
echo "FAIL: require-trusted-types-for 'script' missing from CSP" >&2
exit 1
fi
echo "PASS: Trusted Types enforcement directive present"
The script exits non-zero when the directive is absent, blocking promotion before an unprotected build reaches production.
Related
Permalink to "Related"- Trusted Types & DOM XSS Prevention — the full adoption workflow: inventorying sinks, creating policies, routing, enforcing, and DOMPurify integration
- Coordinating SRI, CSP & Trusted Types — layering this directive with
require-sri-forand ascript-srcallow-list for defense in depth - Configuring Content Security Policy with SRI — the CSP header syntax this directive extends