ZyVOP Logo
Content That Connects
SeriesAI NewsLeaderboardWrite for Us
ZyVOP Logo
Content That Connects

Empowering developers and creators with cutting-edge insights, comprehensive tutorials, and innovative solutions for the digital future.

Content

  • Categories
  • Tags
  • Badges
  • Leaderboard
  • Write Article
  • Newsletter

Company

  • About Us
  • API Documentation
  • Write for Us
  • Contact

Connect

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • DMCA Policy
  • Code of Conduct

© 2026 ZyVOP. Crafted with care for the developer community.

Made with ❤️ by the ZyVOP team
All systems operational
HomeTrusted Types Is Baseline: DOM XSS Is Now a Type Error

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

Danny Holloran
Danny HolloranSenior Developer
August 11, 2026
3 min read
Trusted Types Is Baseline: DOM XSS Is Now a Type Error
#frontend#tooling#web-apis#JavaScript
👍1

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.

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.

Comments (0)

Login to post a comment.

Related Posts

Angular 22: The End of Boilerplate and the Consolidation of the Reactive Era

If you have been following the evolution of Google's framework over the last few years, you know it has been undergoing a silent reconstruction — piece by piece...

Read article

Tiled Rasterization for Large DOM Captures

SnapDOM moves cropping before image decode, turning huge DOM captures into bounded canvas tiles without sacrificing resolution or allocating one enormous bitmap.

Read article

How I Added Eye-Catching Animations to My Frontend Projects ✨

Have you ever visited a website and thought: "How did they make that animation?! 😳" I definitely have. As a frontend developer, I've always been fascinated by ...

Read article

The Long Animation Frames API: Find What Actually Broke Your INP

Your field data says INP is 400ms. Your local profile says everything is fine. The Long Animation Frames API closes that gap by naming the script, the function, and the character position that stalled the frame.

Read article

Misusing React Context, Then Blaming React Context

Stop blaming React Context for unnecessary re-renders. Discover how React composition affects rendering performance and how to build efficient Context providers.

Read article