Trusted Types Put DOM Injection Sinks Behind Policies

DOM-based cross-site scripting often appears at the last step of a data flow. A value moves through application code as an ordinary string, then reaches an API that interprets it as HTML, script, or a script URL. The dangerous boundary is not the string alone; it is the moment that string enters an injection sink.

Trusted Types changes that boundary. In supporting browsers, an application can require covered sinks to receive objects such as TrustedHTML, TrustedScript, or TrustedScriptURL instead of raw strings. Those objects are created through policies defined by the application.

The control does not decide which HTML is safe. It creates an enforcement point where the application has to make that decision explicitly.

Enforcement belongs in CSP

Creating a Trusted Types policy by itself does not stop other code from assigning strings directly to a sink. Enforcement comes from Content Security Policy.

Content-Security-Policy: require-trusted-types-for 'script'; trusted-types app-html;

The require-trusted-types-for 'script' directive requires Trusted Types at covered DOM XSS injection sinks. The trusted-types app-html directive restricts policy creation to the listed policy name.

With enforcement active, a direct string assignment to a covered sink can fail with a TypeError:

const target = document.querySelector("#output");
target.innerHTML = userControlledString;

Application code instead creates a policy and passes a typed value:

const policy = trustedTypes.createPolicy("app-html", {
  createHTML(value) {
    return sanitizeHTML(value);
  },
});

const target = document.querySelector("#output");
target.innerHTML = policy.createHTML(userControlledString);

sanitizeHTML() in this example is application-supplied logic. Trusted Types does not provide a sanitizer and does not prove that a policy transformation is correct.

Policy code becomes a narrow security boundary

Without enforcement, sanitization can be scattered across call sites. One path may sanitize a value while another reaches the same sink directly. Trusted Types makes the distinction visible in the type accepted by the sink.

A policy can centralize the transformation:

input
  |
  +--> ordinary application processing
  |
  v
policy boundary
  |
  +--> validation / sanitization
  |
  v
TrustedHTML
  |
  v
HTML sink

This structure is useful only when policy functions are strict. A policy that simply returns its input converts an arbitrary string into a trusted object without reducing risk.

Policy creation therefore deserves the same scrutiny as other privileged boundaries. The trusted-types CSP directive narrows which named policies may be created, reducing the chance that unrelated code can create an unexpected permissive policy.

TrustedHTML is not a general safety certificate

A TrustedHTML object records that a value passed through a policy capable of producing TrustedHTML. Its security meaning depends on that policy.

A policy may sanitize HTML, escape data, select content from a fixed template set, or reject input that does not match an application-specific rule. Those choices have different semantics. The browser enforces the type boundary, not the quality of the transformation.

This distinction prevents a common overstatement: Trusted Types does not make arbitrary markup safe. It moves authority to create sink-compatible values into explicit policy code.

The same principle applies to TrustedScript and TrustedScriptURL. Their presence marks a controlled construction path for values consumed by corresponding sinks; it does not establish that the resulting script behavior is benign.

Default policies are migration tools, not a final boundary

Trusted Types supports a policy named default. When enforcement is active, a default policy can receive string values that legacy code still sends to covered sinks and return the appropriate trusted value.

That can reduce disruption during migration, but it also changes the failure mode. Instead of exposing every remaining raw-string assignment immediately, the default policy can convert those assignments.

A migration can use this behavior deliberately: instrument the default policy, identify legacy sink calls, move those call sites to explicit policies, then remove the compatibility path. Leaving a broad default policy indefinitely can hide the boundaries that enforcement was meant to expose.

If the default policy returns null or undefined for a value, the sink operation fails rather than accepting that value.

Report-only deployment can expose breakage before enforcement

A mature application may contain sink usage in framework code, third-party packages, templates, or old utility functions. Enabling enforcement without inventory can break legitimate paths.

CSP report-only mode can help surface violations before the enforcing header is deployed:

Content-Security-Policy-Report-Only: require-trusted-types-for 'script'; trusted-types app-html;

Reports are evidence of code paths that need review, not proof that every reported assignment is exploitable. The useful unit of work is the sink call and the data flow that reaches it.

A practical migration separates three cases: a sink that can be removed, a sink that can accept a safer API, and a sink that genuinely needs a Trusted Types policy. Reducing sink use is often simpler than wrapping every existing assignment.

Safer DOM APIs still matter

Trusted Types is not a reason to route ordinary text through HTML parsing. If an application needs to display text, textContent keeps that operation outside an HTML injection sink:

target.textContent = userControlledString;

Likewise, DOM construction APIs can express structure without assembling markup strings. Trusted Types is most valuable where an application truly needs APIs that interpret HTML, script, or script URLs.

This keeps policy count small. A few narrowly scoped policies are easier to review than a large set of policies that mirror existing call sites.

Browser enforcement is one layer

Trusted Types addresses a specific client-side boundary. It does not replace output encoding, context-sensitive sanitization, CSP source restrictions, server-side validation, dependency review, or secure framework patterns.

It also cannot protect a browser that does not implement the relevant enforcement. Compatibility therefore belongs in deployment planning, especially when an application supports older clients.

The useful security property is narrower and concrete: in a supporting browser with the CSP directive enforced, covered injection sinks cannot freely consume ordinary strings. Code must cross an explicit policy boundary or the operation fails.

That converts an implicit convention—remember to sanitize before this sink—into a browser-enforced contract around selected DOM injection points.

References