← Back to DevBytes

Implementing Trusted Types API in Modern Web Applications

Understanding Trusted Types

Trusted Types is a browser security mechanism designed to eliminate DOM-based cross-site scripting (XSS) vulnerabilities at the runtime level. It forces developers to use secure, typed values when inserting potentially dangerous strings into injection sinks like innerHTML, document.write, or eval.

At its core, Trusted Types introduces a set of type-checked wrappers—TrustedHTML, TrustedScript, and TrustedScriptURL—that act as the only valid currency for dangerous DOM operations. The browser simply refuses to accept raw strings in these contexts, effectively making XSS impossible through known injection vectors.

The Problem Trusted Types Solves

Traditional XSS defenses rely on developer diligence: manually escaping output, using CSP headers, and running static analysis tools. These approaches are fragile. A single missed sanitization in a complex template, a third-party library, or a dynamic code path can reintroduce vulnerabilities. Trusted Types shifts the paradigm from opt-in security to secure-by-default by enforcing types at the browser level.

Consider this classic vulnerability pattern:

// Dangerous: user-controlled string flows directly into innerHTML
const userBio = location.hash.substring(1);
document.getElementById('bio-container').innerHTML = userBio;
// Attacker injects: 

With Trusted Types enforced, the browser throws a TypeError before any execution occurs, because a raw string is not a TrustedHTML instance. The attack surface collapses to zero.

Why Trusted Types Matters

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

DOM XSS accounts for a significant percentage of real-world web vulnerabilities. Despite widespread awareness and frameworks with built-in escaping, injection sinks remain pervasive—in legacy code, third-party analytics scripts, ad integrations, and WYSIWYG editors. Trusted Types provides a structural guarantee that no string-based injection can succeed.

The key benefits are:

How Trusted Types Works

The browser maintains an internal registry of "trusted type" policies. When code attempts to assign a value to a dangerous sink, the browser checks whether the value is a known Trusted Type object. If it's a plain string, the assignment fails. The only way to create Trusted Type objects is through explicitly registered policies, which must contain sanitization or validation logic.

The three Trusted Types correspond to three categories of sinks:

Enabling Trusted Types

Trusted Types is enforced via Content Security Policy headers. You enable it with the require-trusted-types-for directive. The simplest configuration is:

Content-Security-Policy: require-trusted-types-for 'script'

This tells the browser to enforce Trusted Types for all script-related injection sinks. You can also add a trusted-types directive to restrict which policies are allowed, further locking down the surface:

Content-Security-Policy: require-trusted-types-for 'script'; trusted-types myAppPolicy dompurify

You can serve this header from your server or set it via a <meta> tag, though the meta tag approach only supports report-only mode:

<meta http-equiv="Content-Security-Policy" 
      content="require-trusted-types-for 'script'; trusted-types default">

Report-Only Mode

Before enforcing, you can run in report-only mode to identify all the places where your codebase violates Trusted Types without breaking functionality. Violations are reported via the securitypolicyviolation event or a reporting endpoint.

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

You can also listen for violations in JavaScript:

document.addEventListener('securitypolicyviolation', (event) => {
  console.log('Trusted Types violation:', {
    blockedURI: event.blockedURI,
    violatedDirective: event.violatedDirective,
    originalPolicy: event.originalPolicy,
    sourceFile: event.sourceFile,
    lineNumber: event.lineNumber,
    columnNumber: event.columnNumber,
    sample: event.sample
  });
  // Send to your analytics or monitoring service
  fetch('/csp-report', {
    method: 'POST',
    body: JSON.stringify({
      blockedURI: event.blockedURI,
      violatedDirective: event.violatedDirective,
      sample: event.sample
    })
  });
});

Creating Trusted Type Policies

Policies are factories that produce Trusted Type instances. You create them using the trustedTypes.createPolicy() API. Each policy defines callback functions for createHTML, createScript, and createScriptURL. These callbacks receive the raw input string and must return a sanitized or validated value.

Basic Policy Example

if (window.trustedTypes && trustedTypes.createPolicy) {
  // Create a default policy that sanitizes HTML using DOMPurify
  trustedTypes.createPolicy('default', {
    createHTML: (input) => {
      // Use DOMPurify to strip dangerous markup
      return DOMPurify.sanitize(input, { RETURN_TRUSTED_TYPE: true });
    },
    createScriptURL: (input) => {
      // Only allow URLs from trusted origins
      const parsed = new URL(input, window.location.origin);
      const allowedOrigins = ['self', 'cdn.example.com', 'apis.example.com'];
      if (allowedOrigins.includes(parsed.hostname) || parsed.protocol === 'https:') {
        return input;
      }
      throw new TypeError('Script URL from untrusted origin blocked');
    },
    createScript: (input) => {
      // Generally, you should never create scripts from strings
      // This is here for completeness; in practice, reject all
      throw new TypeError('Dynamic script creation is not allowed');
    }
  });
}

Working with TrustedHTML

Once you have a policy, you use it to wrap any HTML before injecting it into the DOM:

// Before (vulnerable):
// element.innerHTML = userProvidedMarkup;

// After (secure):
const policy = trustedTypes.getPolicy('default');
const trustedHtml = policy.createHTML(userProvidedMarkup);
element.innerHTML = trustedHtml;

// Or assign directly if your policy returns TrustedHTML
element.innerHTML = policy.createHTML(userProvidedMarkup);

Notice that DOMPurify.sanitize() with RETURN_TRUSTED_TYPE: true returns a native TrustedHTML object when Trusted Types is active. This makes integration seamless with libraries that already support Trusted Types.

Working with TrustedScriptURL

When loading dynamic scripts or creating Web Workers, you must use TrustedScriptURL:

// Creating a Web Worker safely
const policy = trustedTypes.getPolicy('default');

// This will throw if the URL doesn't pass validation
const workerUrl = policy.createScriptURL('/scripts/worker.js');
const worker = new Worker(workerUrl);

// Dynamic script loading
function loadScript(src) {
  const trustedSrc = policy.createScriptURL(src);
  const script = document.createElement('script');
  script.src = trustedSrc; // Now safe to assign
  document.head.appendChild(script);
}

// Use with caution, always validate
loadScript('https://cdn.example.com/trusted-library.js');

Working with TrustedScript

For eval and Function() constructor calls, you need TrustedScript. In almost all cases, you should avoid creating these entirely:

// The safe approach: reject all dynamic script creation
trustedTypes.createPolicy('strict', {
  createScript: (input) => {
    // Log the attempt for auditing
    console.warn('Blocked eval attempt:', input.substring(0, 100));
    throw new TypeError('eval() and Function() are completely disabled');
  },
  createHTML: (input) => {
    return DOMPurify.sanitize(input, { RETURN_TRUSTED_TYPE: true });
  },
  createScriptURL: (input) => {
    // Validate and allow
    return validateAndReturnURL(input);
  }
});

// If you absolutely must support eval for legitimate reasons (rare):
trustedTypes.createPolicy('eval-policy', {
  createScript: (input) => {
    // Cryptographically verify the input came from a trusted source
    // e.g., check a hash or signature before allowing
    const allowedHashes = ['sha256-abc123...'];
    const hash = computeSHA256(input);
    if (allowedHashes.includes(hash)) {
      return input;
    }
    throw new TypeError('Untrusted script rejected');
  }
});

Default Policies and the Fallback Mechanism

The browser allows you to designate a "default" policy that automatically wraps raw strings passed to sinks. While this eases migration, it introduces risk: a misconfigured default policy can become a bypass. Use default policies only temporarily during migration, and always lock down your trusted-types CSP directive to enumerate allowed policies explicitly.

// Default policy (migration-only, remove after migration is complete)
trustedTypes.createPolicy('default', {
  createHTML: (input) => {
    console.warn('Default policy used for HTML—audit this path');
    return DOMPurify.sanitize(input, { RETURN_TRUSTED_TYPE: true });
  },
  createScriptURL: (input) => {
    // Be very permissive here? NO—restrict even during migration
    console.warn('Default policy used for script URL');
    const url = new URL(input, window.location.origin);
    if (url.protocol === 'https:' || url.protocol === location.protocol) {
      return input;
    }
    throw new TypeError('Blocked by default policy');
  }
});

// CSP to restrict to specific policies
// Content-Security-Policy: require-trusted-types-for 'script'; trusted-types mySanitizer dompurify

Handling Third-Party Libraries

One of the biggest challenges is third-party code that uses injection sinks internally. Libraries like analytics SDKs, ad networks, and chat widgets often assign raw strings to innerHTML. When Trusted Types is enforced, these libraries break.

Strategies for handling third-party code:

DOM Setter Override Example

// Override innerHTML to auto-sanitize when Trusted Types is active
// WARNING: This is a stopgap, not a permanent solution
(function() {
  const originalDescriptor = Object.getOwnPropertyDescriptor(
    Element.prototype, 'innerHTML'
  );
  
  Object.defineProperty(Element.prototype, 'innerHTML', {
    set: function(value) {
      if (typeof value === 'string') {
        // Attempt to route through a policy
        const policy = trustedTypes.getPolicy('default');
        if (policy) {
          const trusted = policy.createHTML(value);
          originalDescriptor.set.call(this, trusted);
          return;
        }
      }
      originalDescriptor.set.call(this, value);
    },
    get: originalDescriptor.get,
    enumerable: true,
    configurable: true
  });
})();

Integration with Modern Frameworks

React

React internally uses text nodes and avoids raw innerHTML assignments, making it largely compatible. The dangerouslySetInnerHTML prop, however, requires TrustedHTML. React's DOM renderer checks for Trusted Types support and handles it gracefully when you pass a TrustedHTML object:

import DOMPurify from 'dompurify';

function SafeHtml({ markup }) {
  const trustedHtml = DOMPurify.sanitize(markup, { RETURN_TRUSTED_TYPE: true });
  return 
; }

Angular

Angular's DomSanitizer service integrates with Trusted Types. When Trusted Types is active, Angular's bypass methods (bypassSecurityTrustHtml, etc.) return native Trusted Type objects:

import { DomSanitizer } from '@angular/platform-browser';

@Component({ /* ... */ })
export class SafeComponent {
  trustedHtml: TrustedHTML;
  
  constructor(private sanitizer: DomSanitizer) {
    const rawHtml = 'User-provided markup';
    this.trustedHtml = this.sanitizer.bypassSecurityTrustHtml(rawHtml);
    // In Angular with Trusted Types, this returns a TrustedHTML object
  }
}

Lit (Web Components)

Lit's template system uses tagged template literals, which naturally avoid string-based injection. When binding to properties that map to sinks (like innerHTML), Lit respects Trusted Types:

import { html, render } from 'lit';
import DOMPurify from 'dompurify';

const unsafeHtmlString = '';
const trustedHtml = DOMPurify.sanitize(unsafeHtmlString, { RETURN_TRUSTED_TYPE: true });

// Lit's unsafeHTML directive works with TrustedHTML
render(html`
`, document.body);

Building a Complete Implementation

Let's walk through a production-ready Trusted Types setup for a typical web application. We'll cover CSP headers, policy creation, monitoring, and gradual enforcement.

Step 1: CSP Header Configuration

# nginx configuration example
add_header Content-Security-Policy-Report-Only "require-trusted-types-for 'script'; trusted-types app-policy dompurify; report-uri /csp-violations" always;

# After auditing violations, switch to enforcement:
# add_header Content-Security-Policy "require-trusted-types-for 'script'; trusted-types app-policy dompurify" always;

Step 2: Policy Initialization Module

// trusted-types-init.js — Load this before any application code

(function initTrustedTypes() {
  if (!window.trustedTypes) {
    console.warn('Trusted Types not supported in this browser');
    return;
  }

  // Load DOMPurify (assume it's available or dynamically import)
  const dompurify = window.DOMPurify;

  // Create the main application policy
  try {
    trustedTypes.createPolicy('app-policy', {
      createHTML: (input, ...args) => {
        // Context-aware sanitization
        // args can carry context: e.g., { sink: 'innerHTML', elementTag: 'div' }
        const context = args[0] || {};
        
        if (context.allowEmpty && !input.trim()) {
          return dompurify.sanitize('', { RETURN_TRUSTED_TYPE: true });
        }
        
        return dompurify.sanitize(input, {
          RETURN_TRUSTED_TYPE: true,
          ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li'],
          ALLOWED_ATTR: ['href', 'title', 'target', 'rel']
        });
      },
      
      createScriptURL: (input) => {
        // Allow same-origin and specific CDN origins
        const url = new URL(input, window.location.origin);
        const trustedOrigins = new Set([
          window.location.origin,
          'https://cdn.example.com',
          'https://static.example.com'
        ]);
        
        if (trustedOrigins.has(url.origin)) {
          return input;
        }
        
        // Log and reject
        console.error('Blocked script URL from untrusted origin:', url.origin);
        throw new TypeError(`Script URL origin ${url.origin} is not trusted`);
      },
      
      createScript: () => {
        // Hard reject all eval-equivalent operations
        throw new TypeError('eval and Function constructor are disabled');
      }
    });
    
    console.log('Trusted Types policy "app-policy" registered successfully');
  } catch (error) {
    console.error('Failed to create Trusted Types policy:', error);
  }
})();

Step 3: Safe DOM Helper Functions

// dom-helpers.js — Wrappers for common DOM operations

const policy = trustedTypes.getPolicy('app-policy');

export function setSafeHTML(element, htmlString, context = {}) {
  if (!element) return;
  
  try {
    const trusted = policy.createHTML(htmlString, context);
    element.innerHTML = trusted;
  } catch (error) {
    // Fallback: set to empty or error message
    element.innerHTML = policy.createHTML('<span class="error">Content blocked</span>');
    reportViolation('setSafeHTML', htmlString, error);
  }
}

export function insertAdjacentSafeHTML(element, position, htmlString) {
  try {
    const trusted = policy.createHTML(htmlString);
    element.insertAdjacentHTML(position, trusted);
  } catch (error) {
    reportViolation('insertAdjacentHTML', htmlString, error);
  }
}

export function createSafeScriptURL(urlString) {
  try {
    return policy.createScriptURL(urlString);
  } catch (error) {
    reportViolation('createScriptURL', urlString, error);
    throw error;
  }
}

export function loadSafeScript(urlString) {
  const trustedUrl = createSafeScriptURL(urlString);
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = trustedUrl;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
  });
}

function reportViolation(operation, input, error) {
  console.warn(`Trusted Types violation in ${operation}:`, error.message);
  
  // Send to monitoring
  if (navigator.sendBeacon) {
    navigator.sendBeacon('/api/csp-violations', JSON.stringify({
      operation,
      inputSample: input.substring(0, 200),
      error: error.message,
      timestamp: Date.now(),
      url: window.location.href
    }));
  }
}

Step 4: Auditing Existing Code

Before enabling enforcement, audit all injection sinks in your codebase. Use the violation reporting to build a comprehensive map:

// audit-script.js — Run during report-only phase to collect violation data

const violations = [];

document.addEventListener('securitypolicyviolation', (event) => {
  if (event.violatedDirective === 'require-trusted-types-for') {
    violations.push({
      sourceFile: event.sourceFile,
      lineNumber: event.lineNumber,
      columnNumber: event.columnNumber,
      sample: event.sample,
      timestamp: Date.now()
    });
    
    // Store in localStorage for later analysis
    const existing = JSON.parse(localStorage.getItem('tt-violations') || '[]');
    existing.push(violations[violations.length - 1]);
    localStorage.setItem('tt-violations', JSON.stringify(existing.slice(-1000)));
  }
});

// Export for analysis
window.getTrustedTypesViolations = () => violations;

Step 5: Graceful Migration Strategy

// migration.js — Phase-based rollout

const MIGRATION_PHASES = {
  REPORT_ONLY: 'report-only',
  ENFORCE_WITH_DEFAULT: 'enforce-with-default',
  ENFORCE_NO_DEFAULT: 'enforce-no-default',
  LOCKED_DOWN: 'locked-down'
};

let currentPhase = MIGRATION_PHASES.REPORT_ONLY;

export function getMigrationPhase() {
  // Read from a server-side flag or URL parameter
  const urlFlag = new URLSearchParams(window.location.search).get('tt-phase');
  return urlFlag || localStorage.getItem('tt-phase') || MIGRATION_PHASES.REPORT_ONLY;
}

export function setMigrationPhase(phase) {
  localStorage.setItem('tt-phase', phase);
  currentPhase = phase;
  
  // Update CSP via meta tag or server-side
  switch (phase) {
    case MIGRATION_PHASES.REPORT_ONLY:
      // Keep report-only, collect violations
      break;
    case MIGRATION_PHASES.ENFORCE_WITH_DEFAULT:
      // Enable enforcement with default policy
      document.querySelector('meta[http-equiv="Content-Security-Policy"]')
        ?.setAttribute('content', 
          "require-trusted-types-for 'script'; trusted-types app-policy default");
      break;
    case MIGRATION_PHASES.ENFORCE_NO_DEFAULT:
      // Remove default policy, only allow explicit policies
      document.querySelector('meta[http-equiv="Content-Security-Policy"]')
        ?.setAttribute('content', 
          "require-trusted-types-for 'script'; trusted-types app-policy dompurify");
      break;
    case MIGRATION_PHASES.LOCKED_DOWN:
      // Strictest: single policy, no exceptions
      document.querySelector('meta[http-equiv="Content-Security-Policy"]')
        ?.setAttribute('content', 
          "require-trusted-types-for 'script'; trusted-types app-policy");
      break;
  }
}

Testing and Debugging

Trusted Types violations manifest as TypeError exceptions in the console. During development, use these techniques to debug:

// Debugging helpers
function inspectTrustedTypesEnvironment() {
  console.log('Trusted Types supported:', !!window.trustedTypes);
  console.log('Registered policies:', trustedTypes.getPolicyNames());
  
  // Test a raw string assignment
  try {
    const testDiv = document.createElement('div');
    testDiv.innerHTML = '<strong>test</strong>';
    console.log('Raw HTML assignment: ALLOWED (no enforcement or default policy active)');
  } catch (e) {
    console.log('Raw HTML assignment: BLOCKED (enforcement active)', e.message);
  }
  
  // Test with a trusted value
  const policy = trustedTypes.getPolicy('app-policy');
  if (policy) {
    const trusted = policy.createHTML('<strong>safe</strong>');
    const testDiv = document.createElement('div');
    testDiv.innerHTML = trusted;
    console.log('Trusted HTML assignment: SUCCESS');
  }
}

inspectTrustedTypesEnvironment();

Best Practices

Automated Testing Example

// cypress-test.js — Example Cypress test for Trusted Types compliance

describe('Trusted Types Compliance', () => {
  beforeEach(() => {
    // Inject CSP meta tag before application loads
    cy.intercept('GET', '**/index.html', (req) => {
      req.continue((res) => {
        res.body = res.body.replace('</head>', 
          '<meta http-equiv="Content-Security-Policy" ' +
          'content="require-trusted-types-for \'script\'; ' +
          'trusted-types test-policy"></head>');
      });
    });
  });

  it('should not throw Trusted Types violations on page load', () => {
    cy.on('uncaught:exception', (err) => {
      if (err.message.includes('TrustedHTML') || 
          err.message.includes('TrustedScript') ||
          err.message.includes('require-trusted-types-for')) {
        // Fail the test if a TT violation occurs
        return false;
      }
      return true;
    });
    
    cy.visit('/');
    cy.get('#main-content').should('be.visible');
  });

  it('should safely render user-generated content', () => {
    cy.visit('/user-profile?bio=<img src=x onerror=alert(1)>');
    // Verify no violation and content is sanitized
    cy.get('#bio-content').should('not.contain', 'onerror');
  });
});

Conclusion

Trusted Types represents a fundamental shift in how we prevent XSS vulnerabilities. Rather than relying on developers to never make mistakes, it enforces type safety at the browser level, making string-based injection into dangerous sinks architecturally impossible. The implementation path—starting with report-only CSP headers, creating sanitization policies, auditing violations, and gradually moving toward strict enforcement—allows teams to adopt this protection incrementally without breaking existing functionality.

The investment pays off dramatically: once Trusted Types is fully enforced with a well-configured policy, DOM XSS vulnerabilities are eliminated as a class. Combined with a strong Content Security Policy, secure framework defaults, and regular dependency audits, Trusted Types gives your application defense-in-depth that no amount of manual escaping can match. The API is stable, supported in all Chromium-based browsers, and increasingly adopted by major libraries—making now the ideal time to integrate it into your web security strategy.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles