DEV Community

abhilashlr
abhilashlr

Posted on

🔥 A Complete, In-Depth Guide to Trusted Types in React and Modern Web Apps

Trusted Types in React: A Complete Engineering Guide to Preventing XSS Without Breaking Your App

Trusted Types is one of the most powerful — yet misunderstood — additions to modern browser security. It addresses a problem every frontend engineer encounters eventually: how do we safely render untrusted HTML without opening the door to XSS?

If you’ve ever used:

  • dangerouslySetInnerHTML
  • HTML sanitizers
  • Script injection cleanup
  • Rich text editors
  • User-generated content
  • Markdown → HTML pipelines
  • Server-rendered CMS content

you’ve already danced around this problem.

This article is a deep-dive into how Trusted Types work, why they matter, and how to integrate them with React, Shadow DOM, and real-world production codebases — without rewriting your entire app.


1. Why Trusted Types Exist

The real problem: "string-to-DOM" APIs are unsafe

APIs like:

element.innerHTML = userInput;
iframe.srcdoc = "...";
script.text = "...";
Enter fullscreen mode Exit fullscreen mode

allow arbitrary strings to be interpreted as DOM or script. If any of that string is user-controlled → you have XSS.

For years, sites relied on sanitizers like DOMPurify. These help, but they can be bypassed if:

  • The sanitizer is outdated
  • Someone forgets to sanitize
  • A dev uses a different code path
  • A new dangerous API is introduced
  • Sanitization only happens sometimes

Modern apps use dozens of 3rd-party libraries, each of which might use string → DOM APIs internally.

Trusted Types fixes this structurally.


2. What Trusted Types Actually Do

Trusted Types enforce that any DOM API that expects HTML accepts only a special object, not a string.

Example:

element.innerHTML = "<img src=x onerror=alert(1)>"
// ❌ CSP violation → browser blocks it
Enter fullscreen mode Exit fullscreen mode

But:

const html = myPolicy.createHTML("<b>Hello</b>");
element.innerHTML = html;
// ✅ allowed
Enter fullscreen mode Exit fullscreen mode

The browser now enforces:

  • Only approved code can turn strings into HTML
  • That conversion happens through one policy
  • Every unsafe sink is protected by the browser

This is way more robust than “remembering to sanitize everything”.


3. How React Renders HTML (important!)

React normally sets the HTML for you, not with innerHTML, but through:

<div dangerouslySetInnerHTML={{ __html: html }} />
Enter fullscreen mode Exit fullscreen mode

Internally, React intentionally validates HTML and works through its DOM diffing layers.

But this API still hits an XSS sink.

When Trusted Types are enabled, the browser prevents React from passing plain strings unless they come from a TrustedHTML object.

Thus:

dangerouslySetInnerHTML={{ __html: "<p>hello</p>" }}
// ❌ blocked by Trusted Types
Enter fullscreen mode Exit fullscreen mode

But:

dangerouslySetInnerHTML={{ __html: policy.createHTML("<p>hello</p>") }}
// ✅ safe
Enter fullscreen mode Exit fullscreen mode

4. Setting Up Trusted Types in React

Step 1 — Define a policy

// global script before React mounts
const ttPolicy = window.trustedTypes?.createPolicy("default", {
  createHTML: (string) => DOMPurify.sanitize(string),
});
Enter fullscreen mode Exit fullscreen mode

Important:

Trusted Types do not sanitize.
They only ensure you don’t forget to sanitize.

That’s why you plug in a sanitizer (DOMPurify is the industry standard).


Step 2 — Use the policy where HTML is passed to React

<div
  dangerouslySetInnerHTML={{
    __html: ttPolicy.createHTML(userHTML),
  }}
/>
Enter fullscreen mode Exit fullscreen mode

Note:
ttPolicy.createHTML() returns a TrustedHTML object, not a string.

React doesn’t care — it’ll pass it straight to the DOM.


5. Why createHTML() Is Not a String

This is the most confusing part for developers.

Trusted Types are opaque, branded objects with no .toString().

This is by design:

  • You should not be able to convert them casually
  • They should not flow back into string contexts
  • They must be clearly treated as final sanitized safe DOM fragments

This prevents:

const safe = ttPolicy.createHTML("<p>Hi</p>");
someAPI(safe + "<script>");
Enter fullscreen mode Exit fullscreen mode

This would convert it back to string — which is unsafe.

Trusted Types eliminate this entire class of bugs.


6. Enforcing Trusted Types with CSP

To actually enforce Trusted Types, you set a CSP header:

Content-Security-Policy:
  require-trusted-types-for 'script';
  trusted-types default;
Enter fullscreen mode Exit fullscreen mode

You can also add multiple policies:

trusted-types default dompurify sanitize bleed;
Enter fullscreen mode Exit fullscreen mode

Now every call like:

innerHTML = "<div>"
Enter fullscreen mode Exit fullscreen mode

...is blocked by the browser.


7. Full Example: Rendering User HTML Safely With Trusted Types

Setup the policy

const sanitizePolicy = trustedTypes.createPolicy("sanitize", {
  createHTML: (input) => DOMPurify.sanitize(input),
});
Enter fullscreen mode Exit fullscreen mode

React component

function SafeHtml({ html }) {
  return (
    <div
      dangerouslySetInnerHTML={{
        __html: sanitizePolicy.createHTML(html),
      }}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

Usage

<SafeHtml html={userComment} />
Enter fullscreen mode Exit fullscreen mode

8. Trusted Types in Shadow DOM (Web Components)

Shadow DOM has separate DOM trees, but Trusted Types work the same.

Example inside a custom element:

class SecureWidget extends HTMLElement {
  set content(html) {
    this.shadowRoot.innerHTML =
      sanitizePolicy.createHTML(html);
  }
}
Enter fullscreen mode Exit fullscreen mode

If you forget:

this.shadowRoot.innerHTML = htmlString;
// ❌ CSP violation
Enter fullscreen mode Exit fullscreen mode

Trusted Types enforce safety inside shadow roots too.


9. Where Developers Usually Get It Wrong (Important)

❌ Mistake 1 — Using Trusted Types without sanitizers

Trusted Types stop accidental sinks, but they do not magically clean HTML.

❌ Mistake 2 — Creating multiple policies

Use one policy per pipeline.

Multiple policies = inconsistent security.

❌ Mistake 3 — Using it only in React components

Any of these are sinks too:

  • element.insertAdjacentHTML
  • script.srcdoc
  • iframe.srcdoc
  • range.createContextualFragment()

Your policy should cover every path HTML enters the DOM.


10. Advanced Architecture: A Full HTML Safety Pipeline

A robust real-world pipeline looks like this:

1. Input comes from server/client
               ↓
2. Clean with a sanitizer (DOMPurify)
               ↓
3. Convert via Trusted Types policy
               ↓
4. Render via React/shadow DOM
Enter fullscreen mode Exit fullscreen mode

This ensures:

  • no bypass paths
  • no forgotten sanitization
  • browser-level enforcement

11. Real-World Use Cases

1. Rich text editors

Quill, TinyMCE, TipTap — all output HTML.

Trusted Types ensure no editor misconfiguration breaks your app.

2. CMS-rendered content

Blog platforms, Notion embeds, marketing pages.

3. User-generated comments

Forums, reviews, chat apps.

4. Markdown rendering

Markdown → HTML is extremely risky if not sanitized.


12. Final Check: Is Your App Safe Without Trusted Types?

If you use any of these:

  • dangerouslySetInnerHTML
  • <iframe srcdoc="...">
  • HTML sanitization
  • Web Components with string HTML
  • 3rd-party libraries injecting HTML
  • UI frameworks like Angular/Old React
  • Markdown-to-HTML conversion

→ Your app should use Trusted Types.

Modern security requirements (e.g. enterprise customers, finance, healthcare, gov) increasingly require it.


13. Rolling Out Trusted Types Safely: Report Violations Before Enforcing

A common mistake teams make is enabling:

Content-Security-Policy: require-trusted-types-for 'script';
Enter fullscreen mode Exit fullscreen mode

...immediately and breaking half the app.

In reality, the best practice is a two-phase rollout:

Phase 1 — Report‐Only Mode (No Breaking, Just Visibility)

Before you enforce Trusted Types, configure your CSP with:

Content-Security-Policy-Report-Only:
  require-trusted-types-for 'script';
  report-uri https://YOUR-ERROR-MONITORING-ENDPOINT;
Enter fullscreen mode Exit fullscreen mode

OR for Sentry:

Content-Security-Policy-Report-Only:
  require-trusted-types-for 'script';
  report-uri https://sentry.io/api/<project>/security/?sentry_key=<key>;
Enter fullscreen mode Exit fullscreen mode

This allows browsers to send violation events without blocking the app.

You'll start receiving reports any time a library or component incorrectly uses:

  • innerHTML
  • insertAdjacentHTML
  • script.srcdoc
  • iframe.srcdoc
  • Range.createContextualFragment()
  • React components with raw strings

This gives you a map of all unsafe sinks in your app — something nearly impossible to discover manually.

Phase 2 — Send Violations to Your Error Monitoring Tool

You can also catch violations directly in JavaScript and forward them to Sentry, LogRocket, Datadog, or any internal error system.

window.addEventListener("securitypolicyviolation", (e) => {
  // Example: report to Sentry
  Sentry.captureMessage("Trusted Types Violation", {
    level: "warning",
    extra: {
      directive: e.violatedDirective,
      blockedURI: e.blockedURI,
      lineNumber: e.lineNumber,
      columnNumber: e.columnNumber,
      sourceFile: e.sourceFile,
    },
  });
});
Enter fullscreen mode Exit fullscreen mode

Now every Trusted Types violation becomes visible in error dashboards long before users notice breakage.

Phase 3 — Enforce Trusted Types Once Violations Are Near Zero

Only after fixing the violations should you switch from:

Content-Security-Policy-Report-Only: ...
Enter fullscreen mode Exit fullscreen mode

to the enforcing version:

Content-Security-Policy:
  require-trusted-types-for 'script';
  trusted-types default sanitize;
Enter fullscreen mode Exit fullscreen mode

This two-phase strategy is widely used in Enterprise apps, consumer apps with large legacy codebases

It ensures that rolling out Trusted Types is safe, predictable, and does not break production.


Conclusion

Trusted Types are the single most important browser security feature most frontend devs still underestimate. They provide structural protection from XSS regardless of:

  • who writes the code
  • which library you use
  • how complex your UI is

Adding them to a React or Shadow DOM app is not just simple — it’s the kind of engineering discipline that pays off for years.

Top comments (0)