🚀AdvancedSecurity

Trusted Types & HTML Sanitization

Prevent DOM-based XSS by enforcing Trusted Types policies for innerHTML and dangerous DOM sinks, and use the HTML Sanitizer API for safe HTML insertion.

Overview

DOM-based Cross-Site Scripting (XSS) attacks exploit dangerous DOM sinks APIs like `innerHTML`, `outerHTML`, `document.write()`, and `eval()` to inject and execute malicious scripts. Trusted Types is a browser security mechanism that prevents untrusted strings from reaching these sinks by requiring all assignments to pass through a trusted type policy. With Trusted Types enforced (via CSP header), assigning a plain string to `innerHTML` throws a TypeError. You must create a `TrustedHTML` value through a registered policy, which serves as a code review checkpoint all HTML that reaches the DOM goes through your sanitization policy. The companion HTML Sanitizer API (`new Sanitizer()`) provides a declarative way to strip dangerous HTML while preserving safe formatting a browser-native alternative to libraries like DOMPurify.

Key Points

  • •Enable via CSP: `require-trusted-types-for "script"` header enforces TrustedHTML on innerHTML and similar
  • •`trustedTypes.createPolicy("name", { createHTML: (str) => sanitize(str) })` registers a policy
  • •`policy.createHTML(str)` returns a `TrustedHTML` object safe to assign to `innerHTML`
  • •`new Sanitizer()` HTML Sanitizer API, strips scripts/event handlers while keeping safe formatting
  • •`element.setHTML(untrustedString, { sanitizer })` safely insert HTML using Sanitizer
  • •`el.setHTMLUnsafe(str)` inserts HTML including declarative shadow DOM (still sanitized by browser)
  • •Trusted Types prevents typosquatting attacks on your own code and third-party libraries

Examples

Trusted Types policy for safe HTML insertion

<!-- Enable Trusted Types via CSP header (set on server):
     Content-Security-Policy: require-trusted-types-for 'script'
-->

<div id="output"></div>

<script>
// Register a Trusted Types policy
const policy = trustedTypes.createPolicy('sanitize', {
  createHTML: (input) => {
    // Only allow specific safe HTML
    return input
      .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '') // strip scripts
      .replace(/\son\w+\s*=/gi, '')                      // strip event handlers
      .replace(/javascript:/gi, '');                        // strip javascript: URLs
  }
});

// Get user-controlled content
const userContent = '<h2>Title</h2><script>alert("XSS")</script><p>Safe text</p>';

// ❌ This would throw TypeError with Trusted Types enforced:
// document.getElementById('output').innerHTML = userContent;

// ✅ Must go through policy.createHTML()
const trusted = policy.createHTML(userContent);
document.getElementById('output').innerHTML = trusted;

// Result: <h2>Title</h2><p>Safe text</p> (script stripped)
</script>

In production, use a hardened sanitizer like DOMPurify instead of a naive regex replacement.

The HTML Sanitizer API (browser-native)

<!-- HTML Sanitizer API available in Chrome/Edge -->
<div id="safe-output"></div>

<script>
const userHtml = `
  <h2>Article Title</h2>
  <p onclick="alert('xss')">Click me</p>
  <p style="color:red"><em>Styled</em> paragraph</p>
  <script>alert("xss")<\/script>
  <a href="javascript:void(0)" onclick="steal()">Click</a>
  <a href="https://safe.example.com">Safe link</a>
`;

// Default sanitizer: removes scripts and dangerous attributes
const sanitizer = new Sanitizer();

const output = document.getElementById('safe-output');
output.setHTML(userHtml, { sanitizer });

// Result: headings, paragraphs (without onclick), styled em, safe anchor
// Scripts, javascript: hrefs, and onclick are stripped

// Custom sanitizer: allow only specific elements and attributes
const strictSanitizer = new Sanitizer({
  allowElements: ['p', 'strong', 'em', 'a'],
  allowAttributes: { 'a': ['href'] },
  blockElements: ['script', 'style'],
});

output.setHTML(userHtml, { sanitizer: strictSanitizer });
</script>

setHTML() with Sanitizer is the modern alternative to elem.innerHTML = DOMPurify.sanitize(str).

Default Trusted Types policy for gradual migration

<script>
// During migration: log all unsafe innerHTML uses instead of throwing
// This helps identify all sinks in existing code

if (trustedTypes.defaultPolicy === null) {
  trustedTypes.createPolicy('default', {
    createHTML: (str) => {
      // Log the call for audit
      console.warn('Unsafe innerHTML at:', new Error().stack);

      // In migration phase: allow but log
      // In production: throw or sanitize
      return str; // or DOMPurify.sanitize(str)
    },
    createScript: (str) => str,
    createScriptURL: (str) => str,
  });
}
</script>

A "default" policy is used as fallback when no other policy wraps the value useful for auditing legacy code.

Common Mistakes

  • Writing a Trusted Types policy that just returns the input unchanged this defeats the entire purpose; always sanitize inside the policy
  • Using regex to sanitize HTML HTML is too complex for regex; use DOMPurify, the Sanitizer API, or a whitelist-based parser
  • Forgetting that Trusted Types only protects DOM sinks it does not prevent server-side XSS or reflected XSS in HTML attributes

Note

DOMPurify is the gold-standard HTML sanitizer compatible with Trusted Types: `const clean = DOMPurify.sanitize(dirty, { RETURN_TRUSTED_TYPE: true })`. It returns a TrustedHTML value directly, integrating seamlessly with a Trusted Types policy.

Exercises

Write HTML to solve each exercise. Use Preview to see it render in a live iframe, then mark as done.

Loading editor…

Completed: 0 / 5 exercises

Knowledge Check

Question 1 of 4

What type of XSS does Trusted Types specifically prevent?