← Back to DevBytes

Trusted Types API: Complete Guide

Introduction to Trusted Types

The Trusted Types API is a browser security feature designed to prevent DOM-based cross-site scripting (XSS) vulnerabilities at the source. Introduced as part of the Content Security Policy (CSP) Level 3 specification, Trusted Types restricts dangerous DOM sinks — functions and properties that can execute arbitrary HTML or JavaScript — so that they only accept specially crafted, trusted values rather than raw strings.

Traditional XSS defenses rely on sanitizing input and escaping output, but these approaches are fragile. A single missed sink, such as element.innerHTML or eval(), can open the door to attackers. Trusted Types flips the model: instead of hoping developers remember to sanitize, the browser enforces that only values produced by a vetted policy can flow into dangerous sinks.

Why Trusted Types Matters

DOM-based XSS occurs when user-controlled data is written into a DOM sink without proper sanitization. Consider this common pattern:

// Dangerous: user input flows directly into innerHTML
const userInput = new URLSearchParams(location.search).get('name');
document.getElementById('greeting').innerHTML = 'Hello, ' + userInput;

If userInput contains <img src=x onerror=alert(1)>, the script executes. Escaping helps, but developers must remember to do it everywhere, and new code constantly introduces new sinks.

Trusted Types addresses this by making the dangerous sinks refuse plain strings entirely. Once the policy is enforced, the browser throws a TypeError whenever a raw string is assigned to a sink. This forces all string-to-DOM conversions to go through a small number of audited policy functions, dramatically reducing the attack surface.

Common Dangerous Sinks

Trusted Types protects many sinks, including:

Enabling Trusted Types

Trusted Types is enabled through the Content-Security-Policy header using the require-trusted-types-for directive and the trusted-types directive to declare allowed policies.

Report-Only Mode

Before enforcing, you should run in report-only mode to discover violations without breaking your application:

Content-Security-Policy-Report-Only: require-trusted-types-for 'script';
                                     trusted-types myPolicy;
                                     report-to csp-endpoint

This logs violations to your reporting endpoint but does not block execution. Once the reports are clean, switch to enforcement.

Enforcement Mode

Content-Security-Policy: require-trusted-types-for 'script';
                          trusted-types myAppPolicy sanitizeHTML;
                          report-to csp-endpoint

With this header, any assignment of a plain string to a script sink triggers a TypeError, and only the named policies myAppPolicy and sanitizeHTML may be created.

Creating Trusted Type Policies

A policy is a factory that produces trusted values. You create one with trustedTypes.createPolicy(), passing a policy name and an object with creator functions for each trusted type.

// Define a policy that creates TrustedHTML
const htmlPolicy = trustedTypes.createPolicy('myAppPolicy', {
  createHTML: (input) => {
    // Sanitize the input before trusting it
    return sanitize(input); // your sanitizer, e.g. DOMPurify.sanitize(input)
  }
});

// Now use the policy to produce a trusted value
const trustedHtml = htmlPolicy.createHTML(userInput);
document.getElementById('greeting').innerHTML = trustedHtml;

The createHTML function receives the raw string and must return a sanitized string. The returned value is a TrustedHTML object, which the browser accepts in innerHTML and similar sinks.

Policy Creator Functions

Policies can define up to three creator functions, each producing a different trusted type:

  • createHTML(input) — returns TrustedHTML for sinks like innerHTML
  • createScript(input) — returns TrustedScript for sinks like eval()
  • createScriptURL(input) — returns TrustedScriptURL for script.src
const scriptUrlPolicy = trustedTypes.createPolicy('scriptUrlPolicy', {
  createScriptURL: (url) => {
    const allowed = new URL(url, location.origin);
    // Only allow same-origin scripts
    if (allowed.origin !== location.origin) {
      throw new Error('Cross-origin scripts are not allowed');
    }
    return allowed.href;
  }
});

const script = document.createElement('script');
script.src = scriptUrlPolicy.createScriptURL('/static/app.js');
document.body.appendChild(script);

Working with Default and Fallback Policies

When migrating a large codebase, you may not be able to update every sink immediately. Trusted Types provides a default policy that is invoked automatically whenever a plain string is assigned to a sink. This is useful as a transitional measure.

trustedTypes.createPolicy('default', {
  createHTML: (input) => {
    console.warn('Default policy used for HTML. Input:', input);
    return DOMPurify.sanitize(input);
  },
  createScriptURL: (url) => {
    // Only allow known-safe script URLs
    return url;
  },
  createScript: (script) => {
    throw new Error('Refusing to create trusted script from string');
  }
});

The default policy should be used cautiously. It defeats much of the benefit of Trusted Types because it silently converts strings to trusted values. Treat it as a temporary bridge, not a permanent solution.

Integrating with Frameworks

React

React's dangerouslySetInnerHTML prop is a Trusted Types sink. To use it under enforcement, wrap the HTML in a policy:

const htmlPolicy = trustedTypes.createPolicy('reactHtml', {
  createHTML: (input) => DOMPurify.sanitize(input)
});

function RichText({ content }) {
  return (
    <div
      dangerouslySetInnerHTML={{ __html: htmlPolicy.createHTML(content) }}
    />
  );
}

Angular

Angular has built-in Trusted Types support. The DomSanitizer service produces trusted values that are compatible with the API:

@Component({
  selector: 'app-rich',
  template: '<div [innerHTML]="safeHtml"></div>'
})
export class RichComponent {
  constructor(private sanitizer: DomSanitizer) {}

  safeHtml = this.sanitizer.bypassSecurityTrustHtml(
    DOMPurify.sanitize(this.userContent)
  );
}

Angular also exposes a trustedTypes configuration in angular.json to declare the policies it uses internally.

Handling Third-Party Libraries

Many third-party libraries write to dangerous sinks internally. When you enable enforcement, these libraries will throw errors. You have several options:

  • Use a default policy to sanitize strings transparently during migration.
  • Patch the library by wrapping its sink calls with your policy.
  • Request library updates — many popular libraries now support Trusted Types natively.
  • Allow specific policies for libraries that define their own, by listing them in the trusted-types directive.
// Allow third-party policies alongside your own
Content-Security-Policy: trusted-types myAppPolicy sanitizeHTML goog#html;

The goog#html policy, for example, is used by Google's Closure Library. Listing it explicitly lets the library function while still blocking unlisted policies.

Best Practices

  • Start in report-only mode. Collect violations for weeks before enforcing to understand your full sink surface.
  • Minimize the number of policies. Each policy is an audit boundary. Fewer policies mean fewer places where sanitization logic lives.
  • Use a battle-tested sanitizer. Do not write your own HTML sanitizer inside createHTML. Use DOMPurify or a similar library.
  • Avoid the default policy in production. It is a migration tool. Long-term reliance on it weakens the security model.
  • Throw on unsafe script creation. In your createScript and createScriptURL functions, reject anything that is not on an explicit allowlist.
  • Log policy usage. Instrument your policies to record what inputs flow through them. This helps with auditing and incident response.
  • Combine with other CSP directives. Trusted Types complements script-src, object-src 'none', and other CSP controls. Use them together for defense in depth.

Debugging Violations

When a violation occurs, the browser fires a SecurityPolicyViolation event. You can listen for it to log details:

window.addEventListener('securitypolicyviolation', (event) => {
  console.error('CSP Violation:', {
    violatedDirective: event.violatedDirective,
    blockedURI: event.blockedURI,
    sample: event.sample,
    lineNumber: event.lineNumber,
    columnNumber: event.columnNumber,
    sourceFile: event.sourceFile
  });

  // Send to your logging backend
  navigator.sendBeacon('/csp-reports', JSON.stringify({
    directive: event.violatedDirective,
    sample: event.sample,
    file: event.sourceFile,
    line: event.lineNumber
  }));
});

The sample field contains a truncated snippet of the offending string, which is invaluable for tracking down the source of the violation.

Browser Support and Polyfills

Trusted Types is supported in Chromium-based browsers and has been shipping in Chrome since version 83. Firefox and Safari have varying levels of support, so check current compatibility tables before relying on enforcement alone. For unsupported browsers, the API calls to trustedTypes.createPolicy will simply not exist, and your code should degrade gracefully:

const htmlPolicy = (window.trustedTypes
  ? trustedTypes.createPolicy('myAppPolicy', {
      createHTML: (input) => DOMPurify.sanitize(input)
    })
  : {
      // Fallback: still sanitize, but return a plain string
      createHTML: (input) => DOMPurify.sanitize(input)
    }
);

document.getElementById('greeting').innerHTML =
  htmlPolicy.createHTML(userInput);

This pattern ensures your sanitization logic runs everywhere, while browsers that support Trusted Types get the additional enforcement guarantee.

Conclusion

The Trusted Types API represents a meaningful shift in how the web platform defends against DOM-based XSS. By making dangerous sinks refuse raw strings and channeling all HTML, script, and script-URL creation through audited policies, it converts a sprawling, error-prone problem into a small set of well-defined choke points. Adopting Trusted Types requires effort — inventorying your sinks, writing policies, and working with third-party libraries — but the payoff is a fundamentally smaller attack surface and a browser-enforced guarantee that unvetted strings cannot reach execution contexts. Start with report-only mode, build a minimal set of policies backed by a proven sanitizer, migrate your codebase incrementally, and combine the API with the rest of your CSP strategy for layered, resilient protection.

— Ad —

Google AdSense will appear here after approval

← Back to all articles