-
Notifications
You must be signed in to change notification settings - Fork 1
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.
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-srcthat drops'unsafe-inline'.
init() runs a short, ordered sequence and then reports honestly where it landed:
- 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. - 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 forINJECT_META. - It resolves the sanitizer (your
SANITIZER, elsewindow.DOMPurify) and smoke-tests it once, so a broken sanitizer fails loudly here rather than silently on the first real write. - It claims the
defaultpolicy. This is winner-takes-all: the first code to registerdefaultowns every sink. If something already owns it, DOMFortify will not install and will not vouch for the other policy. - If enforcement is on, the policy is owned, and the sanitizer is ready,
status().protectedis true.
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.
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.