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:
Element.innerHTML,Element.outerHTMLDocument.write(),Document.writeln()Element.insertAdjacentHTML()HTMLScriptElement.src,HTMLScriptElement.texteval(),Function(),setTimeout(string),setInterval(string)Location>,location.href,location.assign()
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)— returnsTrustedHTMLfor sinks likeinnerHTMLcreateScript(input)— returnsTrustedScriptfor sinks likeeval()createScriptURL(input)— returnsTrustedScriptURLforscript.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-typesdirective.
// 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
createScriptandcreateScriptURLfunctions, 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.