Trusted Types: Put a Browser Guardrail Around DOM XSS


Modern frontend applications still turn strings into DOM nodes: a rich-text editor renders user content, a legacy widget injects a template, or a quick feature uses innerHTML. Trusted Types changes that risky boundary from a convention into an enforceable browser policy. Instead of letting arbitrary strings reach DOM XSS sinks, it requires code to pass a deliberately created trusted value.

For teams shipping React, Next.js, or mixed server/client applications, that makes Trusted Types a practical companion to Content Security Policy (CSP): CSP controls which code can load; Trusted Types controls which code can create executable or parsed markup. It is especially useful when third-party packages, CMS content, and AI-assisted changes make a simple “never use innerHTML” rule unrealistic.

Table of contents

  • What problem does Trusted Types solve?
  • How does it work with CSP?
  • Where should a web team start?
  • How do policies fit into a React or Next.js app?
  • What are the limits and rollout traps?
  • A practical rollout checklist

What problem does Trusted Types solve?

Trusted Types addresses DOM-based cross-site scripting (DOM XSS). This class of bug happens after an application has loaded, when JavaScript takes attacker-influenced data and supplies it to a browser API that interprets it as HTML, script, or a script URL. Common sinks include Element.innerHTML, insertAdjacentHTML, and some script-creation paths.

The dangerous part is not that every use of a string is bad. Most strings are harmless text. The problem is that the browser cannot tell the difference between a carefully sanitized HTML fragment and an unreviewed string assembled from a URL parameter, API response, or plugin. A code-review rule can catch some cases, but large applications accumulate exceptions.

With the require-trusted-types-for CSP directive, the browser rejects strings at protected sinks. Code must pass a TrustedHTML, TrustedScript, or TrustedScriptURL value created by a named policy instead. The policy is a narrow, auditable gateway where sanitization or validation belongs.

This is not a replacement for normal output encoding. React’s default escaped interpolation remains the right choice for normal UI. Trusted Types protects the exceptional path: places where an app intentionally asks the browser to parse or execute something.

Treat every HTML-producing path as an API boundary. Trusted Types makes that boundary visible in code and enforceable in the browser.

How does Trusted Types work with Content Security Policy?

Trusted Types is delivered through CSP, but it solves a different layer of the problem. A strong CSP with nonces or hashes can prevent unexpected script from loading. It cannot by itself stop an already-authorized script from passing unsafe markup to innerHTML. Trusted Types adds a constraint inside that authorized code.

A first production policy often looks like this:

http
Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{per-request-nonce}';
  require-trusted-types-for 'script';
  trusted-types app-html dompurify;

require-trusted-types-for 'script' has a historical name: its scope includes DOM APIs that can lead to script execution, including HTML injection sinks. trusted-types is an allow-list for policy names. If code tries to create an unlisted policy, the browser blocks it. That turns policy names into part of the application’s security contract.

For broader CSP context, Chrome’s strict CSP guidance explains why nonces and hashes are safer foundations than ever-growing host allow-lists. The CSP Level 3 specification defines the directives and reporting model. Use both: a nonce-based CSP reduces the chance that untrusted script starts running, and Trusted Types reduces the chance that trusted script turns data into an exploit.

This is also a natural extension of an existing third-party-script program. If you have adopted Integrity-Policy to make external resources accountable, Trusted Types gives the application runtime an equivalent guardrail for parsed strings.

Where should a web team start?

Start in report-only mode, not with a sweeping refactor. Add a reporting endpoint and collect violations in staging and a small production cohort. The browser will identify call sites where a string is being assigned to a protected sink. Those reports are an inventory—not proof that every call site is exploitable—but they replace guesswork with real application paths.

http
Content-Security-Policy-Report-Only:
  require-trusted-types-for 'script';
  trusted-types app-html;
  report-to csp-endpoint;

Then classify each result:

  1. Remove it. Replace raw HTML construction with DOM methods or framework rendering.
  2. Keep it as text. Use textContent, not an HTML parsing API.
  3. Sanitize it. For genuinely rich user or CMS content, sanitize with a maintained HTML sanitizer.
  4. Validate a URL. For script URLs, accept only the exact origins and paths the product needs.
  5. Isolate legacy code. Put the smallest possible policy around a dependency while you plan its replacement.

This classification is more valuable than merely silencing violations. It finds architecture debt, much like package provenance makes teams ask where dependencies really came from.

How do Trusted Types policies fit React and Next.js?

React escapes values rendered in JSX by default, so most components need no Trusted Types code at all. The main audit targets are dangerouslySetInnerHTML, custom DOM manipulation, embedded widgets, Markdown/rich-text rendering, and legacy JavaScript outside React.

Create a small module that is the only place allowed to create a policy. This example uses DOMPurify as the sanitizer; configure it according to the markup your product supports and test that configuration with malicious fixtures.

ts
// lib/trusted-html.ts
import DOMPurify from 'dompurify';

const policy = window.trustedTypes?.createPolicy('app-html', {
  createHTML(input: string) {
    return DOMPurify.sanitize(input, {
      USE_PROFILES: { html: true },
    });
  },
});

export function sanitizeHtml(input: string) {
  return policy ? policy.createHTML(input) : DOMPurify.sanitize(input);
}

At the rendering boundary, preserve the narrow type escape required by current React typings and keep it local:

tsx
function ArticleBody({ html }: { html: string }) {
  const safeHtml = sanitizeHtml(html);
  return <article dangerouslySetInnerHTML={{ __html: safeHtml as string }} />;
}

The fallback is useful for browsers without Trusted Types support, but it is not a security substitute: sanitation must remain correct everywhere. Feature detection lets an application remain functional while supported browsers enforce the stronger contract. MDN’s Trusted Types API reference documents the policy interfaces and compatibility considerations.

In Next.js, take extra care to create policies only in the browser. A module that accesses window during server rendering will fail. Keep this code in a client-only boundary, or defer policy creation until it is called in the browser. Do not solve server rendering failures by weakening the CSP globally.

Trusted Types also pairs well with Next.js observability before hydration: CSP violation reports and client errors should be correlated with release IDs so a blocked sink can be traced to the deployment that introduced it.

What are the most common rollout mistakes?

Creating a permissive default policy. A default policy that simply returns its input can make violations disappear while preserving the vulnerability. If a default policy is unavoidable during migration, make it temporary, instrument it heavily, and give it an expiry date.

Allowing every policy name. trusted-types * loses much of the control. Use a short explicit list, and assign ownership to each name.

Treating sanitization as validation. Sanitizing HTML does not validate a payment URL, an iframe origin, or a script URL. Policies should perform the specific transformation appropriate to the sink.

Breaking vendors without a plan. Analytics, chat, experimentation, and tag tools may inject markup. Before exempting them, establish whether the functionality can use a nonce, a documented integration, a sandboxed iframe, or a safer alternative. The Cookie Store API is a useful reminder that platform APIs can often replace fragile document-level patterns.

Testing only happy paths. Rich text, pasted content, error views, A/B variants, and consent flows often contain the injection paths that normal end-to-end tests skip. Add representative hostile inputs to unit and browser tests.

What should a practical Trusted Types rollout look like?

  1. Map sinks in source code. Search for innerHTML, outerHTML, insertAdjacentHTML, srcdoc, document.write, and dangerouslySetInnerHTML.
  2. Add report-only CSP. Send reports to an endpoint with rate limits and redact unnecessary request data.
  3. Reduce first. Convert simple string interpolation to framework rendering or DOM nodes before introducing policies.
  4. Create named, narrow policies. A CMS HTML policy and a script URL policy should not share a catch-all transformer.
  5. Add a policy allow-list. Start with the names you own; document any dependency exception.
  6. Test with enforcement enabled. Exercise rich content, authenticated routes, embedded integrations, and failure states.
  7. Enforce gradually. Start with internal users or a percentage rollout, monitor violations, then make the directive standard.
  8. Keep the inventory alive. Review new policies in security review, just as you review new third-party scripts.

The goal is not to make HTML impossible. The goal is to ensure every deliberate HTML or script-producing path has a named owner, a specific transformation, and browser enforcement behind it. That is a manageable security upgrade for modern web teams—and a better foundation than hoping every future change remembers the same unwritten rule.

Frequently asked questions

Does Trusted Types replace a CSP nonce?

No. A nonce-based CSP restricts which scripts may execute; Trusted Types restricts risky DOM operations inside scripts that do execute. Strong applications use both.

Does Trusted Types make dangerouslySetInnerHTML safe?

No. It forces supported browsers to require a trusted value when enforcing CSP, but the policy itself must sanitize or validate correctly. Prefer ordinary React rendering whenever rich HTML is not required.

Can I adopt Trusted Types incrementally?

Yes. Begin with report-only CSP, use reports and source searches to inventory sinks, then enforce after policy coverage and testing are in place.

Do all browsers enforce Trusted Types?

Support varies, so retain correct server-side and client-side sanitization. Feature detection keeps the app working where the API is absent, while compatible browsers provide an additional enforcement layer.

Frequently Asked Questions

Does Trusted Types replace a CSP nonce?

No. CSP nonces control which scripts execute, while Trusted Types controls dangerous DOM operations inside those scripts.

Can Trusted Types be adopted incrementally?

Yes. Start with report-only CSP, inventory real violations, then enforce after narrow policies and tests are in place.

Does Trusted Types replace sanitization?

No. A Trusted Types policy must still sanitize HTML or validate URLs correctly; it makes that decision explicit and enforceable.