Trusted Types API Explained: Preventing DOM XSS Structurally
AI generated
JS
() =>
JavaScript · Web Security · DOM XSS
Trusted Types API Explained
ruling out DOM XSS at the type level

DOM based cross site scripting rarely comes from missing server side filtering, it usually comes from unsafe assignments to innerHTML, document.write or eval right in the middle of frontend code. The Trusted Types API closes this gap structurally, letting the browser accept dangerous assignments to DOM sinks only after the value has passed through a defined policy that converts it into a special type.

17 min read TrustedHTML · TrustedScript · TrustedScriptURL Chrome · Edge · Firefox (flag)

1. Why DOM XSS remains possible despite CSP

A Content Security Policy reliably prevents foreign scripts from wrong sources from executing, but it does not prevent your own, ostensibly trustworthy code from handing an unsafe string to innerHTML or document.write. That is exactly where the Trusted Types API comes in: it does not deal with a script's origin, but with the data flow inside the application itself, at the exact point where a string becomes HTML, script or a URL.

A typical example: a search feature assembles result markup with a template literal and assigns it directly to element.innerHTML. If the search term contains a script tag that was not correctly escaped, the browser executes it, with no foreign domain involved and no classic Content Security Policy violation at all. The Trusted Types API prevents exactly this case, because once enabled, innerHTML only accepts special TrustedHTML objects, never raw strings.

Statistically speaking, DOM based cross site scripting vulnerabilities in practice arise more often from seemingly harmless, first party application code than from classic server side injection gaps. That very observation was Google's original motivation for developing the Trusted Types API and implementing it in Chrome.

2. Enabling Trusted Types with require-trusted-types-for

The Trusted Types API is enabled through the Content Security Policy directive require-trusted-types-for 'script'. From that moment on, the browser throws a TypeError exception as soon as a raw string is passed to one of the protected DOM sinks, instead of silently accepting it. This hard failure is intentional: it forces developers to route every assignment explicitly through a policy, instead of making implicit assumptions about a string's safety.

In addition, the trusted-types directive can define which policy names are allowed at all, for example trusted-types default myapp-sanitizer 'allow-duplicates'. Without this restriction, in theory any code in the document could register any number of policies under any name, which would weaken control over the data flow again. Combining require-trusted-types-for and trusted-types forms a two layer safety net.


Content-Security-Policy:
  require-trusted-types-for 'script';
  trusted-types default myapp-sanitizer 'allow-duplicates';

3. Creating policies: createHTML, createScript, createScriptURL

A Trusted Types policy is registered via trustedTypes.createPolicy(name, rules) and defines three possible transformation functions: createHTML for TrustedHTML, createScript for TrustedScript, and createScriptURL for TrustedScriptURL. Each of these functions takes a raw string and must return a sanitized version before the browser accepts it at a protected sink.

The decisive advantage over a manual sanitizer function is enforceability: once require-trusted-types-for 'script' is active, no code can bypass the policy and assign strings directly, because the browser itself enforces the type check. A forgotten sanitizer call at a single code spot no longer leads to a silent security hole, it leads to an immediate exception that becomes visible in tests or in production.


// Register a Trusted Types policy with DOMPurify as sanitizer
const sanitizerPolicy = trustedTypes.createPolicy('myapp-sanitizer', {
  createHTML: (input) => DOMPurify.sanitize(input, { RETURN_TRUSTED_TYPE: false }),
  createScriptURL: (input) => {
    const allowed = ['https://cdn.mironsoft.de/'];
    if (!allowed.some((prefix) => input.startsWith(prefix))) {
      throw new TypeError('Script URL not allowed: ' + input);
    }
    return input;
  },
});

// Now innerHTML only accepts TrustedHTML produced by the policy
function renderSearchResults(container, rawMarkup) {
  container.innerHTML = sanitizerPolicy.createHTML(rawMarkup);
}

4. The default policy for legacy code and third party libraries

If a policy is registered under the reserved name default, it automatically applies to every string passed to a protected sink without an explicit policy. That is especially valuable for large codebases where not every single assignment can immediately be switched to an explicit policy. The default policy then acts as a safety net, routing every unhandled assignment through the same sanitizer logic before it can even fail.

It is important not to mistake the default policy for a permanent solution, but to treat it as a migration tool. Anyone who relies on the default policy long term loses the ability to model different security requirements for different data flows, for example stricter rules for user input versus server generated markup. The recommended path is to gradually move critical code paths to named, specific policies and keep the default policy only for legacy code and third party libraries.


// Fallback policy for legacy code paths not yet migrated
if (window.trustedTypes && trustedTypes.createPolicy) {
  trustedTypes.createPolicy('default', {
    createHTML: (input) => {
      console.warn('Legacy innerHTML assignment sanitized via default policy');
      return DOMPurify.sanitize(input);
    },
  });
}

Beyond the actual sanitizer logic, it pays off to add logging to every policy that records how often and at which code location the policy is actually invoked. This telemetry later makes it easier to decide which code paths should move from the default policy to a more specific, named policy.

5. Protected DOM sinks at a glance

The Trusted Types API protects a fixed list of well known DOM sinks that historically account for most DOM XSS vulnerabilities. These include Element.innerHTML, Element.outerHTML, document.write, document.writeln, the src attribute of script elements, and functions such as eval and setTimeout with a string argument, provided require-trusted-types-for 'script' is active.

Not covered are DOM APIs that are inherently safe, such as textContent or createElement followed by setAttribute for non critical attributes. This distinction matters: the Trusted Types API does not replace general secure coding, it enforces it at exactly the points where a string is factually interpreted as HTML, script or a URL. For every other DOM operation, plain JavaScript remains usable without an additional policy.

6. Violation reporting and gradual migration

Analogous to the Content Security Policy, the Trusted Types API also supports a report only mode via Content-Security-Policy-Report-Only: require-trusted-types-for 'script'. In this mode the browser throws no exception, but reports every violation to the configured reporting endpoint. That allows an entire application's code paths to be observed over an extended period before switching on hard enforcement mode.

In practice a migration in three phases is recommended: first report only with comprehensive logging, then a default policy as a broad safety net, and finally the gradual replacement with named, specific policies for every critical data flow. This order prevents a large legacy codebase from immediately failing with hundreds of errors on the first enforcement attempt.

7. Trusted Types in frameworks and libraries

Modern frameworks like Angular already support Trusted Types natively and internally produce TrustedHTML objects for directives such as [innerHTML]. React works with escaped strings via JSX by default anyway, so it needs Trusted Types directly less often, except with explicit dangerouslySetInnerHTML. For that case, a dedicated policy can be registered that runs React markup through DOMPurify before assignment.

Libraries without native support, for example older jQuery plugins that work directly with .html(), need either a default policy or targeted wrapping of the affected calls. Before migrating it is important to check whether the library in use is even Trusted Types compatible, since some older packages use eval in a way that cannot be cleanly resolved even with a sanitizer policy and requires swapping the library out.

8. Common mistakes and pitfalls

The most common mistake is running the default policy permanently as the only solution, without ever moving critical data flows to specific policies. That leads to user input and server generated markup being treated with the same, often too permissive sanitizer logic. A second mistake is implementing a policy's own sanitizer function unsafely, for example with a custom regex based cleanup instead of a vetted library like DOMPurify.

A third mistake concerns browser compatibility: Firefox and Safari support the Trusted Types API only partially or not at all, so the application must still work without Trusted Types support. A feature detection check via window.trustedTypes before registering a policy prevents the application from crashing with an exception in unsupported browsers.


// Feature detection before registering a policy — required for Firefox/Safari
if (window.trustedTypes && trustedTypes.createPolicy) {
  const policy = trustedTypes.createPolicy('myapp-sanitizer', {
    createHTML: (input) => DOMPurify.sanitize(input),
  });
  window.__cspPolicy = policy;
} else {
  // Fallback: manual sanitization without native enforcement
  window.__cspPolicy = { createHTML: (input) => DOMPurify.sanitize(input) };
}

// Usage stays identical regardless of native support
function safeRender(container, rawMarkup) {
  container.innerHTML = window.__cspPolicy.createHTML(rawMarkup);
}

This kind of feature detection should happen centrally in one single place in the application's bootstrap code, not scattered across every individual spot where a policy is needed. That keeps the compatibility logic maintainable, even as browser support for the Trusted Types API changes in the future.

9. Trusted Types compared to other protections

Trusted Types complements, but does not replace, other protections against cross site scripting. The following table places the Trusted Types API relative to classic sanitization and a Content Security Policy.

Measure Protects against Enforcement Limits
Manual sanitization Known HTML injection patterns Convention, no enforcement Forgotten calls stay invisible
Content Security Policy Foreign script sources, inline scripts Browser enforcement Does not protect against unsafe own DOM code
Trusted Types API DOM based XSS via innerHTML, eval, script src Browser enforcement at type level Limited browser support

In practice all three measures complement each other: a Content Security Policy blocks foreign sources, the Trusted Types API enforces safe conversion at protected sinks, and manual sanitization with a vetted library supplies the actual cleanup logic inside the policy. None of the three measures fully replaces the others.

Mironsoft

DOM security, Trusted Types migration and sanitizer audits

Rule out DOM based cross site scripting structurally?

We identify dangerous DOM sinks in your codebase, design fitting Trusted Types policies and accompany the gradual migration from report only to hard enforcement mode.

Sink audit

Identifying every innerHTML, eval and script src assignment

Policy design

Specific policies per data flow instead of one blanket default policy

Framework integration

Making Angular, React and legacy libraries Trusted Types compatible

Anyone introducing the Trusted Types API into an existing Magento or Hyvä frontend should first check layout XML generated scripts and Alpine.js components for direct innerHTML assignments before enabling the Content Security Policy directive. That prevents central UI components from unexpectedly staying empty after activation.

10. Summary

The Trusted Types API closes a gap that a Content Security Policy alone cannot cover: unsafe assignments to DOM sinks made by your own, trustworthy application code. Enabled through require-trusted-types-for 'script', the browser accepts at protected sinks only values that have already passed through a registered policy. Named policies for critical data flows and a default policy as a migration net complement each other, though the default policy should never be the only permanent solution.

Report only mode, feature detection for unsupported browsers, and integrating existing sanitizer libraries like DOMPurify make adopting the Trusted Types API practically manageable, even in large, grown codebases. Anyone who wants to rule out DOM based cross site scripting not just by convention but through enforced type checking in the browser cannot avoid the Trusted Types API as an additional security layer.

Trusted Types API Explained — The Essentials at a Glance

Four building blocks that together make the Trusted Types API practically manageable in daily work.

Activation

require-trusted-types-for 'script' in the Content Security Policy enforces type checking at protected DOM sinks.

Policies

createHTML, createScript and createScriptURL transform raw strings into trusted types.

default policy

Safety net for legacy code, not a substitute for specific policies per data flow.

Browser support

Chrome and Edge native, Firefox and Safari limited. Feature detection is mandatory.

Together these four building blocks form a practical migration strategy for grown frontend codebases.

11. FAQ: Trusted Types API Explained

1What is the Trusted Types API?
A browser API that makes dangerous DOM sinks accept only policy produced types, preventing DOM XSS structurally.
2How do I enable Trusted Types?
Via require-trusted-types-for 'script' in the Content Security Policy.
3What does a default policy do?
Applies automatically to strings without an explicit policy. Migration net, not a permanent solution.
4Which DOM sinks are protected?
innerHTML, outerHTML, document.write, script src, plus eval and setTimeout with a string argument.
5Do all browsers support Trusted Types?
No, Chrome and Edge natively, Firefox and Safari only partially. Feature detection is mandatory.
6Does Trusted Types replace a sanitizer library?
No, cleanup logic still comes from a library like DOMPurify inside the policy.
7How do I migrate a large codebase?
First report only, then default policy, finally specific policies per data flow.
8Does Trusted Types work with Angular or React?
Angular natively, React less often directly, except with dangerouslySetInnerHTML.
9What happens without feature detection?
createPolicy throws an exception if window.trustedTypes is missing, crashing initialization.
10Can any code register arbitrary policies?
Only without a restriction from the trusted-types directive. An explicit list prevents unauthorized policy registration.