Skip to content

How It Works

Cure53 edited this page Jun 19, 2026 · 2 revisions

How It Works

Trusted Types lets a page register one default policy that the browser consults before every dangerous sink. DOMFortify is that policy. Once enforcement is on, the browser will not let a raw string reach a sink like innerHTML until the default policy has turned it into a trusted value, so DOMFortify gets to sanitize every such write in one place, without the application code changing.

What happens to a DOM write

How a DOM write flows through DOMFortify

The HTML sinks (innerHTML, outerHTML, insertAdjacentHTML, document.write, range.createContextualFragment, and friends) all funnel into the policy's createHTML, which runs the string through your sanitizer and returns clean, trusted HTML. The script sinks (eval, new Function, script.src, javascript: URLs) funnel into createScript and createScriptURL, which refuse by default. Code has no safe subset, so DOMFortify does not pretend to sanitize it; if your app legitimately needs a specific value through, you opt in with ALLOW_SCRIPT or ALLOW_SCRIPT_URL.

Two things on that diagram are easy to miss and matter a lot:

  • It fails closed. If the sanitizer is missing or throws, the HTML sink throws too. DOMFortify never hands the raw markup back on error. A broken sanitizer can break the page, which is the safe direction, so bundle the sanitizer and pin it with SRI.
  • Inline event handlers, style sinks, and URL properties are not Trusted Types sinks, so they bypass all of this. DOMFortify cannot see them. Close them with a script-src that drops 'unsafe-inline'.

How DOMFortify takes over

DOMFortify init lifecycle and the states it reports

init() runs a short, ordered sequence and then reports honestly where it landed:

  1. If the URL matches EXCLUDE, DOMFortify stays completely inactive on that page. It does not install a passthrough, because a silent passthrough would be an XSS hole.
  2. If Trusted Types is unsupported, or enforcement is not active, it stays inert and status() explains why. It does not switch enforcement on by itself unless you asked for INJECT_META.
  3. It resolves the sanitizer (your SANITIZER, else window.DOMPurify) and smoke-tests it once, so a broken sanitizer fails loudly here rather than silently on the first real write.
  4. It claims the default policy. This is winner-takes-all: the first code to register default owns every sink. If something already owns it, DOMFortify will not install and will not vouch for the other policy.
  5. If enforcement is on, the policy is owned, and the sanitizer is ready, status().protected is true.

The default policy is winner-takes-all

This is the single most important operational fact. Whoever registers the default policy first owns every DOM write for the rest of the page's life. So DOMFortify has to run first, inline in <head>, before anything an attacker could reach. If attacker-influenced code registers default before you, you are worse off than if DOMFortify were not there at all, and 'allow-duplicates' in the directive quietly removes that lock, so do not use it. See Risks and Footguns.

A note on reentrancy

When the sanitizer parses your input internally (for example DOMPurify building a throwaway document to inspect it), that inner parsing can re-enter the default policy. DOMFortify detects this synchronous reentry and lets the inner string pass, because it is being parsed in an inert context, not inserted into the live page. This is the same pattern sanitizers use themselves, and it keeps DOMFortify from deadlocking against its own sanitizer.

Clone this wiki locally