React Trusted Types Explained: Preventing XSS in React Apps

Diagram showing untrusted API or CMS content passing through sanitization into a TrustedHTML React rendering boundary, while an unsafe DOM sink is blocked by CSP.

Trusted Types and React: The Short Answer

Trusted Types are a browser-enforced control that restricts certain dangerous DOM injection sinks from accepting ordinary strings. Instead, code must provide a browser-recognized trusted value, such as TrustedHTML, created by a named policy allowed by Content Security Policy (CSP).

Advertisement

For a React application, the practical goal is to make raw HTML rendering an explicit, reviewed boundary. React normally escapes text rendered in JSX, so this is safe:

function Greeting({ name }: { name: string }) {
  return <p>Hello, {name}</p>;
}

If name contains markup-like characters, React renders them as text rather than interpreting them as HTML. That is an important default defense, but it does not eliminate every XSS risk. Applications can still deliberately render HTML, write to DOM APIs directly, load unreviewed third-party code, or mishandle URLs and data across other trust boundaries.

Trusted Types help reduce a specific class of DOM-based injection mistakes. They work with CSP, a sanitizer appropriate to your content rules, React’s existing escaping behavior, and code review. They are not a promise that an application is free of XSS.

A practical implementation path is:

  1. Keep ordinary JSX rendering as the default.
  2. Find every raw-HTML and direct-DOM boundary.
  3. Define precise sanitization rules for the few places that truly need HTML.
  4. Start Trusted Types in CSP report-only mode.
  5. Create a narrowly scoped policy that returns sanitized HTML.
  6. Fix the reported call sites before enforcing the policy.

How XSS Can Still Happen in a React Application

React’s automatic escaping applies to values rendered as JSX children and normal attribute values. It does not mean that all code connected to a React page is safe by default.

Raw HTML is a separate rendering mode

dangerouslySetInnerHTML intentionally passes HTML to the browser:

<article dangerouslySetInnerHTML={{ __html: htmlFromAnApi }} />

This can be appropriate for a carefully controlled rich-text feature, but it creates a distinct trust boundary. The HTML may originate from a CMS, Markdown conversion pipeline, API response, browser storage, imported file, or user-generated content. Its source alone does not establish that it is safe to render.

Review whether the feature needs HTML at all. A comment, title, status message, or plain-text description should normally be rendered as text in JSX. If structured formatting is needed, consider a component-based representation—such as paragraphs, headings, lists, and links from validated data—rather than accepting arbitrary HTML.

Other DOM operations can bypass React’s normal path

Legacy scripts and integration code may assign strings to DOM properties such as innerHTML. Code may also use APIs that parse markup or construct script-like URLs. Trusted Types are designed to protect relevant browser sinks, but the exact behavior depends on the browser and policy configuration, so verify the current authoritative browser and CSP documentation for the environments you support.

Third-party widgets are especially important to inventory. A widget may manipulate a DOM node after React renders it. Development overlays, browser extensions, analytics snippets, rich-text editors, tag managers, and old utility code can all produce violations that do not appear in a search for React components alone.

URLs and application data still need validation

Trusted Types do not validate business data, enforce authorization, protect server-rendered output, or make an unsafe URL safe. For example, links built from user-controlled values need an allowlist of accepted schemes and, where appropriate, allowed destinations. API inputs still need server-side validation. Markdown and CMS workflows still need explicit rules for allowed elements, attributes, and URL protocols.

Think of Trusted Types as a guard at selected browser execution boundaries, not as a replacement for safe data handling throughout the application.

Prerequisites, Browser Support, and Environment Decisions

Before changing CSP, document the environment you are protecting. Record the React rendering model, TypeScript configuration, build tool, server or CDN that delivers headers, browser targets, server-side rendering behavior, and any script-loading mechanism. Check the current React, browser, build-tool, and CSP documentation before publishing or deploying version-specific guidance.

Also list:

  • every use of dangerouslySetInnerHTML;
  • direct writes to DOM nodes;
  • Markdown, CMS, or rich-text pipelines;
  • third-party widgets and legacy scripts;
  • code that runs only in development, previews, staging, or error pages.

Browser support and enforcement behavior are deployment decisions, not assumptions. A browser that does not enforce a Trusted Types policy will not provide the same runtime protection, so retain sanitization and secure rendering practices as baseline controls. Test each browser category your product supports and record the result.

Use separate policies or rollout plans for development, testing, staging, and production when their scripts differ. A permissive development workaround can hide violations that production enforcement would expose. Conversely, production-only integrations can be missed if testing happens only on a local application shell.

Configure a Content Security Policy for Trusted Types

Trusted Types are activated through CSP. Two directives matter most:

  • require-trusted-types-for 'script' asks the browser to require trusted values for protected injection sinks covered by this mechanism.
  • trusted-types restricts which named policy factories application code may create.

The following is an example only. It is not a complete universal CSP, because a safe CSP must reflect your application’s own scripts, styles, workers, frames, connections, and deployment architecture.

Example report-only header

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' 'nonce-<per-response-nonce>';
  require-trusted-types-for 'script';
  trusted-types app-html;
  <retain and review your other project-specific directives>

The policy name app-html must match the name passed to trustedTypes.createPolicy() in your code. If a library needs a separate reviewed policy, add only that exact name after understanding why it needs one. An unrestricted list of policy names weakens the value of having an allowlist.

Move from observation to enforcement

Start with a Content-Security-Policy-Report-Only response header. This lets you observe violations while the application continues to run. Use the browser console and any reporting mechanism your organization has configured to identify the responsible call path. Test real user flows, not only the home page.

Advertisement

After violations have been investigated and corrected, deliver the reviewed policy as an enforcing header:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-<per-response-nonce>';
  require-trusted-types-for 'script';
  trusted-types app-html;
  <retain and review your other project-specific directives>

Do not copy the script-src example blindly. Nonces, hashes, external script hosts, and other directives must be designed for your application. Trusted Types complement a broader CSP; they do not replace it.

HTTP header versus meta element

Delivering CSP as an HTTP response header is generally the better operational choice because it can apply before the document is parsed and can be managed centrally at the application server, reverse proxy, CDN, or hosting layer. It is also the appropriate route for a report-only rollout.

A CSP meta element can be useful where header control is unavailable, but it only takes effect when the browser encounters it. It should be placed as early as possible in the document head and treated as a deployment constraint, not as an equivalent substitute for every header capability. Verify the behavior you rely on in the browsers and hosting setup you actually support.

Create and Use Trusted Types Policies in React Code

A Trusted Types policy is a small, named factory. For an HTML boundary, its createHTML function receives untrusted input and must return sanitized markup. The important design decision is not the policy name; it is the sanitizer configuration and the narrow place where the policy can be used.

The example below assumes that sanitizeRichText is backed by a reviewed HTML sanitizer configured for your application. Do not replace it with a few regular expressions. HTML parsing, attributes, namespaces, malformed markup, and URL protocols make homemade string filtering unsafe.

// trustedHtml.ts
// Replace this declaration with the types provided by your project setup.
declare global {
  interface Window {
    trustedTypes?: {
      createPolicy(
        name: string,
        rules: { createHTML(input: string): string }
      ): { createHTML(input: string): unknown };
    };
  }
}

function sanitizeRichText(untrusted: string): string {
  // Call a reviewed sanitizer here.
  // Define allowed elements, attributes, and URL protocols for this feature.
  throw new Error('Configure a reviewed HTML sanitizer for this application');
}

const richTextPolicy = window.trustedTypes?.createPolicy('app-html', {
  createHTML(untrusted: string) {
    return sanitizeRichText(untrusted);
  },
});

export function createSanitizedHtml(untrusted: string): unknown {
  if (richTextPolicy) {
    return richTextPolicy.createHTML(untrusted);
  }

  // Non-enforcing environments still receive sanitized output.
  return sanitizeRichText(untrusted);
}

Keep the policy module private to the rendering boundary where possible. If every feature can call a broadly named policy, it becomes easier for future code to treat policy creation as permission to render arbitrary HTML.

React’s TypeScript declarations may model __html as a string even when a browser accepts a TrustedHTML value at runtime. Avoid scattering type assertions across the codebase. Isolate any compatibility cast in one small component, document why it exists, and recheck the React and TypeScript type behavior when dependencies change.

// SanitizedHtml.tsx
import { createSanitizedHtml } from './trustedHtml';

type SanitizedHtmlProps = {
  source: string;
};

export function SanitizedHtml({ source }: SanitizedHtmlProps) {
  const html = createSanitizedHtml(source);

  return (
    <div
      className="rich-text"
      dangerouslySetInnerHTML={{ __html: html as string }}
    />
  );
}

The cast is a type-boundary workaround in this illustrative example, not a conversion that makes unsafe input safe. The value must have passed through the reviewed policy first. Keep direct uses of dangerouslySetInnerHTML out of feature components and expose a deliberately named component such as SanitizedHtml instead.

Handling dangerouslySetInnerHTML and Other DOM Sinks

Start by searching the codebase for dangerouslySetInnerHTML, innerHTML, and DOM utility wrappers. For each match, answer four questions:

  1. Is raw HTML necessary, or can this be structured JSX or plain text?
  2. Where does the value originate?
  3. What exact HTML, attributes, and URL schemes does the feature need?
  4. Which sanitizer configuration and Trusted Types policy own this boundary?

Markdown, CMS content, and API-delivered rich text require their own rules. For example, a documentation feature may need headings, paragraphs, lists, code blocks, and selected links. A user bio may need only plain text. Do not reuse a broad rich-text configuration for a field that does not need it.

Sanitize at a narrow, reviewed rendering boundary rather than sanitizing once at ingestion and assuming the value can never change. Depending on your architecture, data can be edited, transformed, cached, migrated, or combined after it is stored. Server-side handling may still be appropriate, but the client-side rendering boundary should not rely on an undocumented historical guarantee.

Never create a policy that simply returns untrusted input unchanged:

// Unsafe: this turns the policy into a bypass.
window.trustedTypes?.createPolicy('app-html', {
  createHTML: (value) => value,
});

A policy is not validation, escaping, authorization, or proof that content is trustworthy. It is the controlled path through which the browser accepts a trusted value for a protected sink.

Common Configuration Errors and Compatibility Problems

The most useful response to a violation is to identify the code path before changing the policy. Common causes include:

  • Policy name mismatch: the code creates legacy-widget, but CSP allows only app-html.
  • Policy placement mismatch: the CSP header or meta configuration is absent from the actual document response, is delivered too late, or differs between environments.
  • Legacy string assignment: old code writes a string to a protected DOM sink.
  • Third-party code: a widget, editor, tag, or dependency manipulates markup internally.
  • Development-only behavior: overlays, hot reload tooling, or extensions alter the page in ways production code does not.
  • Overbroad exception: a team adds a policy name without verifying what its factory returns.

For a third-party library, first determine whether it has a supported configuration or newer release compatible with your target policy. If raw HTML output is genuinely required, wrap or replace the integration at a reviewed boundary. If it requires its own policy, understand and audit the policy behavior before allowing its exact name in CSP. Do not resolve a violation by broadly allowing policy creation or turning off enforcement without documenting the remaining risk.

Browser extensions can also affect console output during local testing. Reproduce suspicious reports in a clean browser profile and compare them with reports from your deployed application before changing production policy.

Verification and Testing Checklist

Trusted Types rollout should have both configuration tests and feature tests. Use this checklist during report-only rollout and again before enforcement:

  • Inspect the document response in browser developer tools and confirm the intended CSP header is present.
  • Confirm that the policy name in CSP exactly matches the policy factory name in code.
  • Exercise routes, forms, error states, dialogs, client-side navigation, and pages that load after the initial route.
  • Test each Markdown, CMS, API, and rich-text rendering path with controlled benign formatting.
  • Use controlled unsafe-looking inputs to confirm they are rejected, removed, or rendered as text according to the feature’s documented rules.
  • Confirm legitimate sanitized content still renders as intended.
  • Review console violations and any configured CSP reports; capture the stack or source path where available.
  • Run automated component and end-to-end tests around the dedicated raw-HTML components.
  • Test third-party integrations, including consent-dependent or delayed widgets.
  • Record results by supported browser category and repeat them after framework, browser-target, sanitizer, or dependency changes.

A useful automated test does more than check that text appears. It should verify the rendering contract: allowed formatting survives, disallowed markup does not become active content, and the component does not throw under the enforcing policy. Keep test inputs representative of the actual content your product allows.

Advertisement

Limitations, Maintenance, and Next Steps

Trusted Types can reduce risk at protected DOM injection sinks, but they do not solve every security problem. They do not prevent server-side injection, insecure authorization, compromised dependencies, unsafe third-party scripts, malicious browser extensions, data exposure, or every form of XSS. React’s escaping, input validation, output encoding appropriate to each context, a reviewed CSP, dependency management, and secure server behavior remain necessary.

Make this configuration maintainable:

  • keep an inventory of raw-HTML boundaries and allowed policy names;
  • review sanitizer rules when content requirements change;
  • review CSP whenever scripts, vendors, deployment infrastructure, or build output changes;
  • treat a new Trusted Types violation as a code-review event rather than a routine exception request;
  • revisit browser support and framework typings during planned dependency upgrades.

The practical next step is small: choose one existing raw-HTML boundary, write down its allowed content rules, place it behind a dedicated sanitizer and component, then run the Trusted Types policy in report-only mode. That gives the team evidence to plan enforcement without claiming that a single control completes the application’s XSS defenses.