ZYVOPMulti-Platform Sync
SeriesAI NewsWhy ZyVOPJoin Discord
LoginGet Started
ZYVOP
The Developer Publishing Hub
SeriesAI NewsPreview My BlogPrivacyTermsGuidelinesDMCACommunity
© 2026 ZyVOP
HomeTrusted Types Is Baseline: DOM XSS Is Now a Type Error

Trusted Types Is Baseline: DOM XSS Is Now a Type Error

August 11, 2026•
3 min read
Danny Holloran
Danny Holloran
Originally published ondanholloran.me
Senior Developer·
Trusted Types Is Baseline: DOM XSS Is Now a Type Error
#frontend#tooling#web-apis#JavaScript

Every codebase has one. Somewhere in a component nobody has opened in eighteen months, there is a line that reads el.innerHTML = someValue, and nobody can tell you with confidence where someValue comes from. Maybe it's a hardcoded template. Maybe it's a server response. Maybe, three refactors ago, it started carrying a slice of location.hash. That uncertainty is the entire DOM XSS problem: the sink is a plain string setter, strings all look alike, and the browser has no way to tell a trusted one from an attacker-controlled one.

Trusted Types fixes that by refusing strings outright. And as of February 2026, when Firefox 148 shipped support, it's Baseline — Chrome and Edge have had it since 83 back in 2020, Safari joined in version 26, and now the whole core browser set is covered. It's no longer a Chrome-only hardening trick you bolt onto an internal admin tool.

Turning a whole class of bug into a runtime error

The API works by locking down the risky sinks: innerHTML, outerHTML, insertAdjacentHTML, document.write, DOMParser.parseFromString, Range.createContextualFragment, script src and text content, and the code-compiling family (eval, new Function(), string-argument setTimeout and setInterval).

Once enforcement is on, passing a raw string to any of them throws a TypeError.

You opt in with a CSP header:

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

That first directive is the switch. The second is an allowlist of policy names — factories that are the only things permitted to mint a trusted value:

const escapeHTMLPolicy = trustedTypes.createPolicy("escape-html", {
  createHTML: (input) => input.replace(/"/g, """),
});

const safe = escapeHTMLPolicy.createHTML(userInput);

safe instanceof TrustedHTML; // true
el.innerHTML = safe; // fine
el.innerHTML = userInput; // TypeError

Note what this actually buys you. Trusted Types does not sanitize anything — your createHTML function is still your own code and can still be wrong. What it guarantees is that every path into a dangerous sink now runs through a named policy you declared on purpose.

The DOM XSS attack surface of the entire app collapses down to the handful of lines inside your policies. That is a security review you can finish in an afternoon instead of grepping 40,000 lines for innerHTML.

Also worth knowing before you start: Trusted Types only works in secure contexts, so HTTPS or localhost.

Rolling it out without breaking production

Do not flip enforcement on first. Ship the report-only variant, let it run against real traffic, and collect what breaks:

Content-Security-Policy-Report-Only: require-trusted-types-for 'script'; report-uri /csp-reports

Violations arrive with the file, line, column, and a script-sample snippet of the offending value, which is usually enough to find the culprit immediately.

If you'd rather not stand up a collector on day one, a ReportingObserver gets you the same data in the console:

new ReportingObserver(
  (reports) => {
    for (const r of reports) {
      if (r.body.effectiveDirective === "require-trusted-types-for") {
        console.warn("Trusted Types violation:", r.body);
      }
    }
  },
  { buffered: true },
).observe();

Then work the list. Most violations have a boring fix — the code didn't need string HTML in the first place:

// before
el.innerHTML = "Click to enlarge";

// after
el.replaceChildren(
  Object.assign(document.createElement("img"), {
    src: "xyz.jpg",
  }),
);

Where you genuinely need to render untrusted HTML, reach for a sanitizer that already speaks the protocol. DOMPurify will hand back a TrustedHTML instead of a string if you ask:

import DOMPurify from "dompurify";

el.innerHTML = DOMPurify.sanitize(html, {
  RETURN_TRUSTED_TYPE: true,
});

The escape hatch is a policy literally named default, which the browser applies to any string that reaches a sink without one. It's the right tool when a third-party script from a CDN is the thing violating and you can't patch it.

Use it grudgingly — a default policy re-centralizes all your sanitization decisions in one function that has no idea what context it's being called from, which is most of the way back to where you started.

Where this fits

Trusted Types and the Sanitizer API solve adjacent halves of the same problem and pair well: the Sanitizer decides what HTML is safe, Trusted Types enforces that something made that decision at all.

Neither replaces a strict, nonce-based CSP for the server-rendered side of XSS.

If you own an app that handles anything sensitive, the report-only header is close to free — one line of config, zero behavior change, and a list of exactly where your DOM XSS risk lives. Start there, and decide later whether to enforce.

Further reading: MDN's Trusted Types API reference and the web.dev deep dive.

Comments (0)

Join the discussion by logging into your account.

No comments yet. Be the first to comment!

Danny Holloran
Danny Holloran

Senior Developer

Senior Frontend & Fullstack Developer with 14+ years building performant, scalable web applications. Passionate about architecture, mentorship, and finding the right tool for the job.

Subscribe to Danny Holloran's Newsletter

Direct email dispatches when new stories are published. Zero algorithms.

Danny Holloran
Like
Love
Clap
Fire
Party
Wow

More from Danny Holloran

View profile

Uint8Array toBase64 and toHex: Stop Round-Tripping Bytes Through btoa

JavaScript can finally turn bytes into base64 and hex (and back) without the String.fromCharCode and btoa dance. Here's how Uint8Array's new encoding methods work and where they replace the helpers you've been copy-pasting.

4 minSep 25

import defer: Lazy Modules Without Going Async

ES2026's import defer evaluates a module the first time you touch its namespace, not at startup, so you can move heavy work off the cold path without turning half your call stack async.

3 minSep 22

Turbopack's Persistent Build Cache Only Helps If Your CI Keeps It

Next.js 16.3 turned on Turbopack's persistent build cache by default and published numbers as high as 5.5x. Most CI pipelines will see exactly zero of that, because the cache is a directory and containers start empty.

4 minSep 21

CSS reading-flow: Fix the Tab Order Your Layout Broke

Grid and flexbox let you rearrange a layout without touching the DOM, which quietly breaks keyboard navigation. The reading-flow and reading-order properties finally let CSS tell the browser which order actually counts.

3 minSep 19

WebNN: The Only Web API That Can Reach Your NPU

Almost every laptop shipped in the last two years has a neural processing unit sitting idle. WebNN is the only web standard that can actually talk to it, and it just hit an updated Candidate Recommendation.

4 minSep 15