WSS
Web Specification Studio Home
On this page
BlogSecurityArchitecturePublished

Trusted Types API: Production Deployment

How to deploy the Trusted Types API without breaking your site: default policy setup, indirect sinks, CSP delivery options, violation reporting, and how webpack, Vite, and Angular interact with require-trusted-types-for.

This guide covers operational behavior of the Trusted Types API and its two Content-Security-Policy directives, trusted-types and require-trusted-types-for, drawn from the W3C specification’s algorithms and current browser behavior. It assumes familiarity with the basic API - policies, TrustedHTML/TrustedScript/TrustedScriptURL, and the report-only-to-enforce migration path - and focuses on behavior relevant to production deployment: default policy semantics, the boundary between direct and indirect sinks, CSP delivery mechanics, violation reporting, and interaction with build tooling and third-party scripts.

Browser support: Trusted Types reached Baseline availability in February 2026 (Chrome/Edge 83+, Firefox 148+, Safari 26+, per MDN compatibility data). This guide assumes a target of browsers implementing the full API and both CSP directives.

Default policy

A TrustedTypePolicyFactory has one default policy slot per realm. trustedTypes.defaultPolicy is null until a policy named default is created, and it can be set once. A second createPolicy("default", ...) call in the same document throws a TypeError.

trustedTypes.createPolicy('default', {});
trustedTypes.createPolicy('default', {}); // throws TypeError

Warning: In applications assembled from multiple independent scripts - a bundler runtime, a tag manager, one or more vendor SDKs - more than one component may attempt to register a default policy, each assuming it is the only one doing so. The first registration succeeds; every subsequent attempt throws. The resulting failure (a silent no-op or an aborted initialization, depending on how the losing script handles the exception) is difficult to reproduce locally, since local environments rarely load the full set of production third-party tags. It typically surfaces as an initialization failure traceable through CSP violation reports.

Register the default policy in one place, as early as possible - a blocking inline script in <head>, not a module and not deferred - so it runs before any other script that might reach an injection sink:

// Must run before ANY other script that might touch an injection sink.
(function () {
  if (!window.trustedTypes || !window.trustedTypes.createPolicy) return;

  if (window.trustedTypes.defaultPolicy) {
    if (process.env.NODE_ENV !== 'production') {
      throw new Error('default policy already registered, check load order');
    }
    return;
  }

  window.trustedTypes.createPolicy('default', {
    createHTML(input, sink) {
      // sink identifies the calling API. Use it for triage, not for
      // varying sanitization rules per sink - see Third-party scripts below.
      reportDefaultPolicyHit('html', sink, input);
      return DOMPurify.sanitize(input, { RETURN_TRUSTED_TYPE: false });
    },
    createScript() {
      // Refuse. Code that needs to eval a script should use its own
      // named policy rather than fall through the default.
      return null;
    },
    createScriptURL(input, sink) {
      reportDefaultPolicyHit('scripturl', sink, input);
      return null; // block by default; allowlist explicitly if needed
    },
  });
})();

Return value semantics

A default policy method that returns null or undefined is not a no-op. The process-value-with-a-default-policy algorithm treats that return value as a signal to throw a TypeError at the sink - a fail-closed design, so a default policy that cannot decide about an input blocks the write rather than letting the original string through.

Note: A createHTML implementation that throws or returns undefined on an unexpected input shape (null, a number coerced to string, an object with a custom toString) surfaces as a runtime error at the point of the DOM write. The stack trace points at the sink, not at the code that produced the malformed value.

Interaction with report-only mode

Content-Security-Policy-Report-Only: require-trusted-types-for 'script' reports violations without blocking sink calls. If a default policy is registered while running in report-only mode, the browser still executes that policy’s transformation function and still writes its return value to the sink - independent of whether enforcement would have blocked the write.

Warning: A default policy registered during report-only testing means violation counts do not reflect unmodified application behavior. They reflect an application already running every covered sink write through the default policy’s transformation. Where that transformation (for example, DOMPurify sanitization) alters markup the application depends on, the application can appear to work throughout the report-only period and then regress once enforcement is turned on - not because enforcement introduced the change, but because it removed the fallback path that had been masking it.

To inventory violations against unmodified behavior, run report-only without a default policy first, and fix call sites directly. Add a default policy afterward, scoped to code that genuinely cannot be modified - a third-party bundle, a legacy iframe - once first-party code no longer relies on it.

Indirect sinks

Direct sinks (innerHTML, Document.write(), eval(), and the rest of the getAttributeType()/getPropertyType()-mapped set) are checked synchronously at the call site.

A separate category, indirect sinks, is checked at a different point in the DOM lifecycle. Building a <script> element with createElement('script') and then appendChilding a text node built from a string triggers no check when the text node is created - a text node carries no signal that it’s destined for script execution.

The check runs when the script element becomes connected and executable: when it’s inserted via appendChild, insertBefore, or an equivalent operation. At that point the user agent evaluates whether the element’s text content arrived through a TrustedScript-producing path. If not, the default policy is consulted; if there isn’t one, the operation throws.

Cross-document behavior

A <script> element populated with a TrustedScript in one document, then moved via cloneNode() or importNode() into a document governed by a different CSP, is re-checked against the destination document’s policy, not the source document’s.

Note: A script element considered trusted in a permissive iframe can be rejected when moved into a stricter top-level document.

SVG script elements

SVGScriptElement maintains its own internal slot and its own WPT test suite (SVGScriptElement-internal-slot.html), independent of HTMLScriptElement. A default policy or sanitizer written only for HTML script insertion needs separate handling for SVG constructed dynamically - common in charting and data-visualization code.

Scope limitation

Note: The specification’s Security Considerations section states that Trusted Types logic runs on many operations that construct DOM trees from strings, but should not be treated as a mechanism for guarding all DOM tree creation in a document.

Template-cloning gadgets, Range.createContextualFragment() chains, and the script-gadget techniques documented in Lekies et al., “Code-Reuse Attacks for the Web: Breaking Cross-Site Scripting Mitigations via Script Gadgets” (ACM CCS 2017), route untrusted markup through a text attribute or a data-binding expression, where a templating library’s already-trusted internal code converts it into executable script. No injection sink is called directly with an untrusted string, so no check is triggered.

Trusted Types reduces the code that needs review for injection risk to the set of policy call sites. It doesn’t verify what those policies do internally once a value is designated trusted.

Attribute enforcement scope

Element.setAttribute() is a listed sink because certain attribute–value pairs require a Trusted Type - not because every attribute write is checked.

getAttributeType(tagName, attribute, elementNs, attrNs) returns a required type only for a specific, enumerated set:

ElementAttributeRequired type
HTMLIFrameElementsrcdocTrustedHTML
HTMLScriptElementsrcTrustedScriptURL
Any elementon* event handler content attributes (onclick, onerror, etc.)TrustedScript
Any elementinnerHTML-equivalent attributes where applicableTrustedHTML

Attributes outside this table aren’t enforced. el.setAttribute('data-config', untrustedJSON) and el.setAttribute('aria-label', untrustedString) both proceed as plain-string writes in either report-only or enforce mode, since getAttributeType returns null for these names.

Note: A common DOM XSS pattern in custom elements and component libraries doesn’t route through onclick. A component reads a data-* or custom attribute via getAttribute and uses it internally - for example, assigning it to innerHTML. Trusted Types enforces that internal assignment if the component’s own code uses a policy for it, but provides no coverage if the component builds unsafe markup from the attribute value without calling a listed sink directly.

Audit attribute-driven components separately. Enforcing Trusted Types doesn’t by itself guarantee coverage of attribute-sourced XSS.

CSP delivery

trusted-types and require-trusted-types-for can be delivered via HTTP header or via <meta http-equiv="Content-Security-Policy">. CSP3 restricts several directives from meta-tag delivery; these two aren’t among them.

DirectivePermitted in <meta>
frame-ancestorsNo
sandboxNo
report-uri / report-toNo
trusted-typesYes
require-trusted-types-forYes
<meta http-equiv="Content-Security-Policy"
      content="require-trusted-types-for 'script'; trusted-types default dompurify webpack">

This is a viable option for static hosting without server-side header control.

Effective range

A meta-delivered CSP takes effect from the point the HTML parser reaches that element onward. Inline scripts and sink calls triggered by markup preceding the meta tag aren’t covered. Place it as early as possible in <head>, before any other <script> element - the same load-order constraint that applies to default policy registration.

Realm scope

Enforcement applies per realm: per top-level document, and separately per worker global and per iframe.

Note: A CSP set on the main document doesn’t propagate automatically to a classic Worker constructed from a blob: URL unless the blob content or the worker’s own response carries equivalent headers. The worker’s script resource is evaluated against whatever policy applies to that fetch. When an enforcing page loads a dynamically constructed worker via new Worker(url), the url argument is itself a TrustedScriptURL sink evaluated in the creating context; behavior inside the worker is governed separately, by that worker’s own CSP delivery.

Violation reporting

A CSP violation report for require-trusted-types-for includes document-uri, source-file, line-number, and script-sample. Two properties of this format affect triage at scale.

blocked-uri carries no sink identity

blocked-uri is always the literal string trusted-types-sink - a fixed constant, not a resource or sink identifier. Determining which API was called (innerHTML, insertAdjacentHTML, document.write, or another sink) requires parsing script-sample instead.

script-sample is truncated

The specification caps the sample length; the WPT suite includes a dedicated test (trusted-types-reporting-clipping-of-sample.html) confirming this. A payload longer than the cap appears only up to that limit.

Note: Often sufficient to identify sink type from context, not sufficient to reconstruct the full injected value. Don’t build an incident-response process that assumes the report contains the full payload.

Third-party noise

On a site loading analytics, advertising, or support-widget scripts, most violations in production typically originate outside the site’s own asset manifest, all reporting require-trusted-types-for as effectiveDirective regardless of origin. Filter on source-file against a known first-party asset list before triage. Where a third-party script can’t be fixed, isolating it in a sandboxed iframe with its own CSP is generally more effective than sanitizing its output through a shared default policy.

const FIRST_PARTY_PREFIXES = [
  location.origin + '/static/',
  location.origin + '/assets/',
];

function isFirstParty(sourceFile) {
  if (!sourceFile) return false; // inline scripts often report no source-file
  return FIRST_PARTY_PREFIXES.some((p) => sourceFile.startsWith(p));
}

new ReportingObserver(
  (reports) => {
    for (const report of reports) {
      if (
        report.type !== 'csp-violation' ||
        report.body.effectiveDirective !== 'require-trusted-types-for'
      ) {
        continue;
      }
      const v = report.body;
      const bucket = isFirstParty(v.sourceFile) ? 'first_party' : 'third_party';

      sendToCollector({
        bucket,
        sourceFile: v.sourceFile,
        line: v.lineNumber,
        column: v.columnNumber,
        sampleClipped: v.sample, // truncated, not the full payload
        disposition: v.disposition, // 'enforce' vs 'report'
      });
    }
  },
  { buffered: true, types: ['csp-violation'] },
).observe();

// Fallback for cases ReportingObserver misses. securitypolicyviolation
// fires synchronously at violation time and catches cases buffered
// reporting can lag on.
document.addEventListener('securitypolicyviolation', (e) => {
  if (e.effectiveDirective !== 'require-trusted-types-for') return;
  const bucket = isFirstParty(e.sourceFile) ? 'first_party' : 'third_party';
  sendToCollector({ bucket, sourceFile: e.sourceFile, line: e.lineNumber });
});

Third-party scripts

A default policy shared across first-party code and every third-party script on a page applies identical rules to two categories with different risk profiles: first-party legacy code, actively being migrated and worth detailed visibility into, and vendor code, which won’t be migrated and primarily needs containment.

The sink parameter passed into default-policy callbacks can separate reporting for these categories:

window.trustedTypes.createPolicy('default', {
  createHTML(input, sink) {
    // sink is a string such as "Element innerHTML" - it identifies the
    // call-site type, not the caller. Use it for routing reports, not
    // for varying what counts as safe for a given tag type.
    const stack = new Error().stack || '';
    const isKnownVendor = /googletagmanager|intercom|segment/.test(stack);

    if (isKnownVendor) {
      reportViolation('vendor', sink, input.slice(0, 100));
    } else {
      reportViolation('first_party', sink, input.slice(0, 100));
    }
    return DOMPurify.sanitize(input);
  },
});

Note: Reading the call stack inside a policy callback isn’t part of the API - the spec exposes no caller-identity signal to policy code. It’s a practical workaround for separating reporting when a shared default policy is unavoidable.

Where integration effort allows, scope each vendor SDK to its own named policy (createPolicy('gtm', {...}), createPolicy('intercom', {...})) with rules limited to what that vendor actually needs - usually script-URL allowlisting, not arbitrary HTML - and remove the shared default policy once first-party code no longer depends on it.

Build tool integration

Bundlers and frameworks that construct DOM trees or load code dynamically are themselves Trusted Types clients. Their policy names and internal sink usage need to be accounted for before enforcement.

ToolBehaviorRecommendation
webpackCreates its own policy to load JS chunks when output.trustedTypes is configured, named webpack by default or set via output.uniqueName. If trusted-types is an allowlist, chunk-loading policy creation fails with a TypeError unless this name is included; under enforcement, code-split lazy routes stop loading.Set output.trustedTypes.policyName explicitly and add it to the trusted-types allowlist.
Vite (dev server)Uses innerHTML internally for HMR style injection (updateStyle). Running require-trusted-types-for in development with a CSP matching production breaks HMR outright rather than only reporting a violation - a known, tracked issue in Vite’s history.Use Content-Security-Policy-Report-Only in development instead of matching production enforcement exactly; enforce only against the built production bundle.
AngularRegisters reserved policy names for specific APIs: angular, and conditionally angular#unsafe-bypass (used by DomSanitizer.bypassSecurityTrust*), angular#unsafe-jit, angular#unsafe-upgrade. A bypassSecurityTrustHtml call fails at the point enforcement is enabled if angular#unsafe-bypass is absent from the allowlist.Add every reserved Angular policy name the application exercises to trusted-types before enabling enforcement.
// webpack.config.js
module.exports = {
  output: {
    trustedTypes: {
      policyName: 'my-app#webpack',
      // 'continue': fall back to string-based chunk loading if policy
      // creation fails (works until enforcement is enabled, then fails
      // loudly at that point). 'stop': fail immediately and visibly.
      onPolicyCreationFailure: 'stop',
    },
  },
};
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types my-app#webpack default dompurify

allow-duplicates and wildcard allowlists

Two trusted-types directive options exist to ease migration, and both reduce the guarantee the specification is built on: a policy reference functions as a capability, so restricting which code paths hold a reference to a given policy restricts which code can produce trusted values.

Directive additionEffectRisk
trusted-types foo 'allow-duplicates'Permits createPolicy('foo', ...) more than once.Any script on the page can register a new policy under a name reviewed code already uses, with independently defined, unreviewed rules.
trusted-types *Permits any policy name.Removes the allowlist entirely; nothing constrains which code can create a named policy.

Both are legitimate during migration of a large codebase where auditing every createPolicy call site immediately isn’t practical, and both are checks PortSwigger’s Trusted Types Checker flags - since both mean enforcing Trusted Types no longer implies that only reviewed code creates trusted values.

Track either as a temporary state with a defined removal plan. The security property Trusted Types is deployed to provide depends on removing them.

See also

From the team at

We build digital products and explore the modern web standards behind them.

Related posts