Browser Security
Published
Trusted Types enforcement is usually rolled out by a security team, and protected JavaScript is usually shipped by a release team, so the two meet for the first time in production. The good news is that the interaction is small, specific, and entirely under your control — but it is worth knowing exactly where the one collision lives rather than discovering it under a strict policy.
What Trusted Types actually governs
Trusted Types is not a general code-integrity feature and it is not a stricter script-src. It is a mechanism for closing DOM injection sinks. When a page sends Content-Security-Policy: require-trusted-types-for 'script', the browser stops accepting plain strings at the places where a string turns into executable code or markup, and demands a typed object minted by a policy you registered.
Two families of sink matter here. The markup family covers innerHTML, outerHTML, insertAdjacentHTML and friends, which now want a TrustedHTML. The script family covers eval, the Function constructor, and script URL assignment such as script.src, which want a TrustedScript or TrustedScriptURL.
That framing answers most of the question immediately. Obfuscation rewrites the identifiers and structure of your JavaScript. It does not decide which DOM APIs your application calls. If your code assigned innerHTML before protection, it assigns innerHTML after protection, and Trusted Types will object in exactly the same place. Protection neither creates nor removes a sink in your own code.
Where protected output can genuinely collide
There is one real collision, and it is narrow and controllable: two options make the engine emit an eval call into your output.
ReorderCodeEval wraps function bodies so that the reordered body is reconstructed and executed through eval at runtime. SelfCompression reaches the same place, because it force-enables the reordering path as part of how it unpacks. If either is on and you enforce require-trusted-types-for 'script', the first protected function to run hits a blocked sink and your application stops.
Every other transform is Trusted-Types-clean. Renaming, string extraction and encryption, control-flow flattening, dead code, member renaming and the lock family all produce ordinary JavaScript that calls ordinary APIs.
Three runtime wrappers are the exception and belong on the same list: SelfDefending, SelfHealing and AntiMonkeyPatching evaluate code of their own while your program runs, and the engine emits a build warning saying they need a policy that permits dynamic evaluation. They are not transforms of your code, which is why they sit outside the two options above, but under enforcement they hit the same blocked sink. Note also that the engine skips all three when your source is an ES module, with its own warning, so on a module build they may never have been applied.
This is the same boundary described in the CSP article, arrived at from a different direction. If you already removed unsafe-eval from your policy, you have already turned those options off, and Trusted Types adds no new work. If you kept unsafe-eval, this is the second reason to reconsider.
What the runtime modules do, checked rather than assumed
A fair question is whether the defensive runtime that protection injects is itself sink-free, since that code you did not write. It is worth being specific rather than reassuring.
The clean-realm comparison used by AntiMonkeyPatching and by the environment-integrity checks needs a pristine copy of the builtins, and it gets one by constructing a hidden iframe. It does that with createElement, then sets style.display and an aria-hidden attribute, and appends it. It never assigns src or srcdoc. Creating an element and setting a style property are not Trusted Types sinks, so the clean-realm path runs unmodified under enforcement.
The third-party script inventory wraps document.createElement so it can record script elements other code creates. Wrapping a function is not a sink either; it observes the elements, it does not inject markup.
One caveat that is not about Trusted Types but bites in the same configuration: the hidden iframe needs frame-src to permit it. The integrity check already handles the refusal explicitly, reporting a realmUnavailable finding rather than a clean result, on the principle that could-not-look is not the same as nothing-found. If you run a tight CSP, expect that finding and read it correctly.
The migration order that avoids a bad afternoon
Trusted Types rollouts fail most often because two changes land together and nobody can tell which one broke the page. Separate them.
First, enforce Trusted Types on your unprotected build and fix the violations that surface. These are yours: a templating helper writing innerHTML, a third-party widget assigning a script URL, a sanitiser that needs to become a policy. This is the bulk of the work and it has nothing to do with obfuscation.
Second, confirm your protection profile has ReorderCodeEval and SelfCompression off. Turn on report-only first if you want the belt and braces.
Third, protect and retest. Because the option set is now sink-free, violations at this stage are almost always a sink in your own code that only executes on a path your unprotected test run missed — not a new one introduced by protection.
Report-only mode deserves a specific note here. Content-Security-Policy-Report-Only gives you the violation stream without breaking anything, and the report names the sink and the source position. Against protected code that position is meaningless on its own, which is what symbolication exists for — and it only works against the protection report that produced that exact build, so archive one per release.
A boundary worth stating plainly
Trusted Types and obfuscation solve problems that only look adjacent. Trusted Types is an integrity control aimed at DOM XSS: it constrains what can become code inside your page, including code injected by an attacker who found a hole in your templating. Obfuscation is a confidentiality control aimed at readers of your shipped source: it raises the cost of understanding logic you were forced to deliver.
Neither substitutes for the other, and the failure mode of pretending otherwise runs both ways. Obfuscated code is exactly as injectable as readable code, because injection targets the sink rather than the surrounding style. And a page with a perfect Trusted Types policy still hands every visitor its complete application logic, which is the problem the protection step is for.
The short version
Trusted Types and protected output coexist without special handling, provided ReorderCodeEval and SelfCompression are off — those two are the only transforms that emit an eval into your code, and the three evaluation-based runtime wrappers (SelfDefending, SelfHealing, AntiMonkeyPatching) are off too, since the engine warns that they need the same permission. The other injected runtime modules use createElement and function wrapping rather than markup sinks, so they are clean under enforcement, though the clean-realm iframe still needs frame-src. Roll out Trusted Types against the unprotected build first, keep a protection report so report-only violations remain readable, and do not expect either control to do the other job.
Frequently asked questions
Does Trusted Types enforcement break obfuscated JavaScript?
Not in general. Trusted Types constrains DOM injection sinks such as innerHTML, the Function constructor, eval and script URL assignment. Obfuscation rewrites identifiers and structure without changing which browser APIs your application calls, so a page that had no violations before protection has none after it. The single exception is the pair of options that emit an eval call into the output, and both are configuration you control.
Which obfuscation options are incompatible with require-trusted-types-for script?
ReorderCodeEval and SelfCompression. The reordering option reconstructs function bodies and executes them through eval at runtime, and self-compression force-enables that same path as part of unpacking. Under script enforcement the first protected function to run hits a blocked sink. Every other transform, including renaming, string encryption, control-flow flattening, member renaming, the lock family and the runtime guards, produces output with no eval and no markup sink.
Do the injected runtime defense modules violate a Trusted Types policy?
No. The clean-realm comparison builds a hidden iframe with createElement, sets a style property and an aria-hidden attribute, and appends it, never assigning src or srcdoc, so it touches no typed sink. The third-party script inventory wraps document.createElement to observe script elements rather than injecting markup, which is also not a sink. The one thing to plan for is unrelated to Trusted Types: the hidden iframe needs frame-src to allow it, and the integrity check reports realmUnavailable rather than a clean result when a policy blocks it.
Is Trusted Types a replacement for obfuscation?
No, and neither replaces the other, because they address different properties. Trusted Types is an integrity control aimed at DOM XSS, constraining what can become executable inside your page. Obfuscation is a confidentiality control aimed at whoever reads the source you were obliged to ship. Obfuscated code is exactly as injectable as readable code, and a page with a flawless policy still delivers its full application logic to every visitor.
How do I roll out Trusted Types on an application that ships protected code?
Change one thing at a time. Enforce the policy on the unprotected build first and fix the violations that appear, since those are sinks in your own code and represent most of the work. Then confirm ReorderCodeEval and SelfCompression are off in the protection profile. Then protect and retest, at which point any remaining violation is almost certainly a code path your first pass did not exercise rather than something protection introduced.
How do I read a Trusted Types violation report from protected code?
Run Content-Security-Policy-Report-Only to collect violations without breaking the page, then translate the reported position with symbolication. The source position in a report against protected output is meaningless by itself, and symbolication only works against the protection report that produced that exact build, so archive a report for every release. Without it the violation tells you the sink but never the line.
Related reading