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.
Read the matrix as a checklist of capabilities: each row you leave without a callback stays permanently blocked, so an application that never injects script URLs should deliberately omit createScriptURL rather than supply a permissive one.
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.
Nothing in that sequence inspects the markup a second time. The sink checks the type of the value it was handed, not its contents, which is why the sanitizer inside createHTML is the entire security boundary — a policy that returns its input unchanged satisfies the browser exactly as well as one that strips every event handler, and a review of your Writing a Trusted Types Policy with DOMPurify callbacks is therefore worth more than a review of every sink assignment in the codebase.
Because this directive governs DOM sinks only — not the bytes a <script src> fetches — pair it with an integrity attribute (plus crossorigin="anonymous" for cross-origin sources) so the fetched code is verified as well as routed.
Variant Examples
Permalink to "Variant Examples"Which of the following you ship is decided by three facts about your deployment: whether you control response headers, whether you have already mapped the sink surface, and whether code you cannot edit writes to sinks. Walk them in that order.
Report-Only rollout
Permalink to "Report-Only rollout"Deploy this first, the same way you would stage any other script control when Rolling Out a Script Policy in Report-Only Mode. 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. Route those reports into the same collector you use for Alerting on SRI Failures from CSP Reports, then alert on the require-trusted-types-for value specifically: a sudden run of them after a deploy usually means a bundle started writing to a sink nobody routed, and the users hitting it are seeing a broken widget rather than a security warning.
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.
Frequently Asked Questions
Permalink to "Frequently Asked Questions"What value does require-trusted-types-for take?
The only defined value is ‘script’. It enables enforcement for the script-related DOM injection sinks such as innerHTML, script.src, and eval. There is no ‘style’ value; the directive is script-scoped by design.
Do Firefox and Safari support require-trusted-types-for natively?
Not as of 2026. Chromium browsers (Chrome, Edge, Opera) enforce it natively. Firefox and Safari require the W3C Trusted Types polyfill loaded before your application script to enforce the directive and to collect violation reports.
Can I send an enforcing and a Report-Only policy at the same time?
Yes. Content-Security-Policy and Content-Security-Policy-Report-Only are evaluated independently, so you can enforce a narrow allow-list today while a Report-Only header trials a tighter one. A common pattern is enforcing trusted-types app-ui default while Report-Only omits default, which surfaces every sink still relying on the fallback policy without breaking those callers.
Does Trusted Types remove the need for SRI on script tags?
No. The directive governs how strings reach DOM sinks; it says nothing about the bytes a script URL returns. A TrustedScriptURL that passes your policy can still point at a CDN file that was swapped after you audited it. Keep integrity and crossorigin on every cross-origin script tag and treat the two controls as separate layers.
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
- Layering CSP Nonces, SRI and Trusted Types — one header that carries nonces,
require-sri-forand this directive together, and the order to roll them out in