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:
- Elimination of DOM XSS: When correctly configured, string-based injection becomes impossible.
- Runtime enforcement: Protection works regardless of framework, templating engine, or developer discipline.
- Auditability: All dangerous sink usage becomes explicit and reviewable.
- Compliance: Helps meet PCI DSS, SOC 2, and other regulatory security requirements.
- Third-party safety: Libraries and scripts you include cannot accidentally introduce XSS through sinks.
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:
TrustedHTML— for sinks likeinnerHTML,insertAdjacentHTML,document.writeTrustedScript— for sinks likeeval,Function()constructor,setTimeoutwith string argumentTrustedScriptURL— for sinks likescript.src,Worker()constructor URL
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:
- Isolate in iframes: Load third-party content in sandboxed iframes with minimal privileges. The iframe's own CSP can be separate.
- Proxy the sink assignments: Override native DOM setters to intercept raw string assignments and route them through your policy. This is fragile but works for stubborn libraries.
- Work with vendors: Request that library vendors support Trusted Types natively. Many major libraries (React, Angular, DOMPurify) already do.
- Use a compatibility shim: As a last resort, use the Trusted Types compatibility polyfill for libraries that haven't adapted.
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:
- Chrome DevTools: Violations appear with detailed stack traces pointing to the exact sink and line number.
- Break on exceptions: Enable "Pause on caught exceptions" in DevTools to catch Trusted Types violations as they happen.
- Policy inspection: Use
trustedTypes.getPolicyNames() in the console to see registered policies.
- Type checking: Use
trustedTypes.isHTML(value), trustedTypes.isScript(value), trustedTypes.isScriptURL(value) to check if a value is trusted.
// 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
- Start with report-only: Never enable enforcement without first collecting violation data. You might discover unexpected breakage in edge cases or third-party code.
- Minimize the number of policies: Each policy is a potential bypass vector if misconfigured. Use one or two well-audited policies rather than creating per-component policies.
- Reject createScript entirely: There is rarely a legitimate need for
eval or the Function() constructor. Block them completely.
- Use DOMPurify with RETURN_TRUSTED_TYPE: DOMPurify is the gold standard for HTML sanitization and integrates natively with Trusted Types.
- Validate ScriptURLs aggressively: Only allow specific, hardcoded origins. Never accept arbitrary URLs for script loading or worker creation.
- Avoid DOM setter overrides in production: Overriding native setters creates maintenance burden and potential inconsistencies. Use them only temporarily.
- Isolate third-party content: Use iframes with
sandbox attributes for ad networks, analytics, and other third-party scripts that perform DOM injection.
- Monitor continuously: Even after enforcement, continue logging violation reports. New features or library updates can introduce new sinks.
- Keep policies simple: Complex policy logic increases the chance of bugs. Prefer straightforward, easily auditable validation rules.
- Test in CI/CD: Include Trusted Types enforcement in your integration tests. Run automated checks that verify all DOM operations succeed under enforcement.
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