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 = "...";
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
But:
const html = myPolicy.createHTML("<b>Hello</b>");
element.innerHTML = html;
// ✅ allowed
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 }} />
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
But:
dangerouslySetInnerHTML={{ __html: policy.createHTML("<p>hello</p>") }}
// ✅ safe
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),
});
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),
}}
/>
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>");
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;
You can also add multiple policies:
trusted-types default dompurify sanitize bleed;
Now every call like:
innerHTML = "<div>"
...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),
});
React component
function SafeHtml({ html }) {
return (
<div
dangerouslySetInnerHTML={{
__html: sanitizePolicy.createHTML(html),
}}
/>
);
}
Usage
<SafeHtml html={userComment} />
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);
}
}
If you forget:
this.shadowRoot.innerHTML = htmlString;
// ❌ CSP violation
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.insertAdjacentHTMLscript.srcdociframe.srcdocrange.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
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';
...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;
OR for Sentry:
Content-Security-Policy-Report-Only:
require-trusted-types-for 'script';
report-uri https://sentry.io/api/<project>/security/?sentry_key=<key>;
This allows browsers to send violation events without blocking the app.
You'll start receiving reports any time a library or component incorrectly uses:
innerHTMLinsertAdjacentHTMLscript.srcdociframe.srcdocRange.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,
},
});
});
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: ...
to the enforcing version:
Content-Security-Policy:
require-trusted-types-for 'script';
trusted-types default sanitize;
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)