Trusted Types and the Sanitizer API prevent cross-site scripting (XSS) attacks by ensuring only sanitized HTML reaches the DOM. Instead of manually filtering user input or relying on external libraries, these browser APIs handle security natively.
The traditional approach with innerHTML accepts any string, making XSS vulnerabilities trivially easy to introduce. One unsanitized user input anywhere in your codebase can compromise the entire application. These new APIs change that by making unsafe HTML insertion either impossible or explicit.
Browser Support
setHTMLUnsafe() is supported in all modern browsers. setHTML() with basic sanitization is also widely available in Chrome, Firefox, Safari, and Edge. The Trusted Types API works in Chrome 83+, Edge, Samsung Internet, Safari, and Firefox Nightly.
The Sanitizer API (for custom configurations) is currently in Firefox Nightly and Chrome Canary behind a flag. Safari has a positive position but hasn’t started implementation yet.
How setHTML() Prevents XSS Attacks
The setHTML() method sanitizes HTML automatically before inserting it into the DOM. Unlike innerHTML, which blindly accepts any string, setHTML() strips out dangerous elements and attributes by default.
Here’s the simplest example: div.setHTML(html). That’s it. No configuration needed for basic protection.
What gets removed? Always: <script>, <iframe>, <embed>, <object>, <frame>, SVG <use> elements, and inline event handlers like onclick or onerror. By default, it also removes much more: <style>, <link>, <img>, <video>, form elements, custom elements, data attributes, ARIA attributes, inline styles, and HTML comments.
Consider this malicious HTML:
const html = `
<h1 onclick="alert('XSS')">Hello</h1>
<button>Click me</button>
<img src="photo.jpg">
<iframe src="https://evil.com"></iframe>
`;
div.setHTML(html);After sanitization, the div contains only <h1>Hello</h1>. Everything else is stripped.
Configuring Sanitizer API Options
The default behavior is aggressive, but you can customize what gets removed. For the most permissive approach, pass an empty sanitizer object: div.setHTML(html, { sanitizer: {} }). This removes only the critical threats: scripts, iframes, embeds, objects, frames, SVG use elements, and inline event handlers.
For tighter control, use a whitelist approach where you explicitly define what’s allowed. Anything not listed gets removed:
const sanitizer = new Sanitizer({
elements: ["h1", "h2", "p", "strong", "a"],
attributes: ["href", "class", "id"]
});
div.setHTML(html, { sanitizer });If you need even finer control, specify allowed attributes on a per-element basis:
const sanitizer = new Sanitizer({
elements: [
{ name: "h1", attributes: [] },
{ name: "a", attributes: ["href", "title"] }
]
});This sanitizer strips all attributes from h1 elements but allows href and title on links.
Alternatively, use a blacklist approach to remove specific elements beyond the defaults:
const sanitizer = new Sanitizer({
removeElements: ["a", "img"],
removeAttributes: ["id", "data-*"]
});How Trusted Types API Works
While setHTML() provides safe-by-default HTML insertion, Trusted Types goes further by enforcing that developers can’t accidentally bypass sanitization. You opt in via a Content Security Policy header:
Content-Security-Policy: trusted-types myPolicy; require-trusted-types-for 'script';Once enabled, passing strings directly to “unsafe sinks” throws a TypeError. Unsafe sinks include innerHTML, outerHTML, setHTMLUnsafe(), document.write(), and setting an iframe’s srcdoc property via JavaScript.
div.innerHTML = "<h1>Hello</h1>"; // ❌ TypeError with Trusted Types
div.setHTMLUnsafe("<iframe>"); // ❌ TypeError with Trusted TypesInstead, you must create a Trusted Types policy that transforms strings into TrustedHTML objects. The policy defines how strings get sanitized:
const policy = trustedTypes.createPolicy("myPolicy", {
createHTML: (input) => {
return DOMPurify.sanitize(input, { RETURN_TRUSTED_TYPE: false });
}
});
const trustedHTML = policy.createHTML(userInput);
div.innerHTML = trustedHTML; // ✅ WorksThe Trusted Types API itself doesn’t sanitize anything, that’s your responsibility. Until the Sanitizer API has full browser support, most policies use DOMPurify for sanitization.
For trusted server-rendered HTML (like declarative shadow DOM), you might use a pass-through policy that returns the input unchanged:
const passthrough = trustedTypes.createPolicy("passthrough", {
createHTML: (input) => input
});
fetch('/trusted-endpoint')
.then(response => response.text())
.then(html => {
const trustedHTML = passthrough.createHTML(html);
target.setHTMLUnsafe(trustedHTML);
});Use pass-through policies sparingly and only for HTML you absolutely trust.
Trusted Types and Sanitizer API Integration
Trusted Types and the Sanitizer API complement each other. The Sanitizer API (via setHTML()) provides safe HTML insertion. Trusted Types enforces that all HTML insertion goes through safe channels, ensuring no developer can accidentally introduce vulnerabilities.
Here’s the key insight: setHTML() and Document.parseHTML() are NOT unsafe sinks. Even with Trusted Types enabled, they accept strings directly. no TrustedHTML objects required. This is because they sanitize by default.
The best practice is straightforward: use setHTML() instead of innerHTML, and use Document.parseHTML() instead of parseFromString(). These methods provide automatic protection without requiring Trusted Types policies.
The Bottom Line
XSS vulnerabilities are among the most common security issues in web applications. setHTML() and Trusted Types give you browser-native protection without external dependencies (though DOMPurify is still useful for Trusted Types policies until Sanitizer API support improves).
Start with setHTML() for automatic protection. Add Trusted Types via CSP to enforce that all DOM manipulation goes through safe paths. Together, they make accidental XSS vulnerabilities much harder to introduce.
// Secure HTML insertion, no library needed
div.setHTML(userContent);Feature detection is simple:
if ('setHTML' in Element.prototype)For older browsers, fall back to DOMPurify or similar libraries.
Note: While setHTML() has excellent browser support, the Sanitizer API for custom configurations is still rolling out. For production use with complex sanitization rules, DOMPurify remains a solid choice until broader Sanitizer API support arrives.
More native APIs: Looking for other browser-native performance wins? Check out the post I wrote about how the CSS Highlights API speeds up syntax highlighting by eliminating DOM spans.