Trusted Types — Cheatsheet
CSP · Trusted Types · HTML Sanitizer API · playground/
Important
Things like innerHTML, eval, and script.src are dangerous: they take plain text that can turn into running code if an attacker controls that text.
Trusted Types (via CSP: require-trusted-types-for 'script' + trusted-types) tells the browser: for those APIs, only accept wrapped values (TrustedHTML, TrustedScript, TrustedScriptURL) from trustedTypes.createPolicy(...)—or, in some cases, a string that goes through a default policy you registered. You put sanitization inside the policy; CSP lists which policy names are allowed, instead of every file doing its own thing.
HTML Sanitizer API (setHTML(), Document.parseHTML(), …) is different: the browser cleans the HTML before it lands in the page. With trusted-types 'none' (“Perfect Types”) you cannot register policies, so you use those Sanitizer paths (not raw innerHTML) for HTML from strings.
Table of contents
- A. HTTP / CSP — report-only, enforce, Perfect Types
- About
DOMPurifyin examples — optional stand-in; nativesetHTML()and other sanitizers - B. Policies &
TrustedHTML— three types, createPolicy → innerHTML, feature-detect, escape-only - C. Default policy (migration) — the
defaultpolicy name for gradual migration - D. What breaks under enforcement — sinks,
TypeError,TrustedScript - E. Safe ways to put HTML on the page — pick one pattern per call site
- F. HTML Sanitizer API & Trusted Types — safe vs unsafe methods + TT interplay
- G. Seeing violations in the browser — ReportingObserver /
securitypolicyviolation - H. Tiny polyfill (old browsers) — tinyfill; see
POLYFILL.md - Links & resources — specs, MDN, web.dev, related posts
A. HTTP / Content-Security-Policy
Tip
Start with headers that only report problems. When nothing important breaks, switch to headers that enforce rules. Always send CSP from your server for browsers that support Trusted Types natively.
A.1 Report-only — log issues, do not block yet
The browser records violations (for example string assigned to innerHTML) but the page keeps working.
Content-Security-Policy-Report-Only: require-trusted-types-for 'script'; report-uri https://my-csp-endpoint.example/
If you use the Reporting API, you can use report-to instead of report-uri. The Trusted Types part is always require-trusted-types-for 'script'.
A.2 Enforce — and list which policy names you allow
You list allowed policy names in trusted-types. Only those names may be passed to trustedTypes.createPolicy():
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types myPolicy
You can add report-uri (or report-to) on the enforcing header too, so you still get logs after you turn blocking on.
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types myPolicy; report-uri https://my-csp-endpoint.example/
A.3 “Perfect Types” — no policies; use the Sanitizer API instead
Note
Here you forbid creating any Trusted Types policy (trusted-types 'none'). Legacy string APIs such as innerHTML = "..." cannot be used in the usual way. You insert HTML with setHTML() or parse with Document.parseHTML() instead. The HTML Sanitizer API behind those methods is Baseline in current Chromium, Firefox, Safari, and Edge.
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types 'none'
Worked examples: E.1 setHTML() and F.4 Document.parseHTML().
About DOMPurify in the examples (it is optional)
Note
Many snippets call DOMPurify.sanitize(...) because upstream docs use it as a well-known stand-in for “turn untrusted HTML into something safer before it hits a sink.” You do not have to use DOMPurify.
Project: DOMPurify (GitHub)
Inside createHTML (and friends), the body can be any approach you trust and can review, for example:
- Native HTML Sanitizer API — often you can skip
innerHTMLentirely and useelement.setHTML(...)orDocument.parseHTML(...)so the browser applies its own XSS-safe rules (see E.1 and F). - Another sanitizer library, or code you wrote yourself, as long as the rules are clear and maintained.
- Strict escaping when you only need plain text, not rich HTML.
Trusted Types do not pick the sanitizer—they only require that data passes through your policy (or through APIs like setHTML() that enforce safety differently). Pick one strategy per call site and stick to it.
B. Policies & TrustedHTML
A policy is a small object you create once. Its methods (like createHTML) turn a string into a trusted value the browser accepts on “dangerous” APIs. Below is the usual introduction to policies and sinks.
B.1 The three trusted types + a minimal example
Browsers group dangerous APIs into a few kinds. Each kind has a matching trusted type. A sink is one of those APIs: without Trusted Types, a string you pass in might be unsafe.
| Type | Typical sinks (examples) | Meaning |
|---|---|---|
TrustedHTML |
innerHTML, outerHTML, insertAdjacentHTML, document.write, document.writeln, DOMParser.parseFromString, iframe srcdoc, … |
The browser treats the value as HTML to parse or insert. |
TrustedScript |
eval, inline <script> text, new Function, setTimeout / setInterval with a string body, … |
The browser treats the value as JavaScript to run. |
TrustedScriptURL |
HTMLScriptElement.src, and other properties that take a URL to a script, … |
The browser treats the value as a script URL to fetch or follow. |
Your policy implements createHTML, createScript, and/or createScriptURL depending on which sinks your code uses.
Tip
Minimal flow: create a policy → wrap the string → assign the trusted value. The createHTML callback can use any sanitizer you choose (here: DOMPurify — see About DOMPurify).
const policy = trustedTypes.createPolicy("my-policy", {
createHTML: (input) => DOMPurify.sanitize(input),
});
const userInput = "<p>I might be XSS</p>";
const element = document.querySelector("#container");
const trustedHTML = policy.createHTML(userInput);
element.innerHTML = trustedHTML;
Note
If your CSP includes trusted-types my-policy, the name you pass to createPolicy must be my-policy (same string).
B.2 Same as B.1, but only if the browser supports Trusted Types
On very old browsers, trustedTypes may be missing. This avoids a hard error before you register the policy:
if (window.trustedTypes && trustedTypes.createPolicy) {
const policy = trustedTypes.createPolicy("myPolicy", {
createHTML: (input) => DOMPurify.sanitize(input),
});
// use: element.innerHTML = policy.createHTML(userHtml);
}
The CSP trusted-types list must include myPolicy if you use that name.
B.3 Escape-only policy (good for plain text, not rich HTML)
Warning
This only escapes special characters. It is not a full sanitizer for rich HTML. Use DOMPurify or setHTML() when users can pass real markup.
Escape-only example:
const escapeHTMLPolicy = trustedTypes.createPolicy("myEscapePolicy", {
createHTML: (string) =>
string
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/"/g, """)
.replace(/'/g, "'"),
});
const el = document.getElementById("myDiv");
const escaped = escapeHTMLPolicy.createHTML('<img src=x onerror=alert(1)>');
el.innerHTML = escaped;
C. Default policy (migration)
If you cannot change every call site yet (for example third-party code still does innerHTML = x), register a policy whose name is exactly default. The browser may send string assignments through that policy.
C.1 Default policy that runs DOMPurify
if (window.trustedTypes && trustedTypes.createPolicy) {
trustedTypes.createPolicy("default", {
createHTML: (string, sink) =>
DOMPurify.sanitize(string, { RETURN_TRUSTED_TYPE: true }),
});
}
Tip
For your own code, prefer a named policy at each call site so rules stay easy to find.
C.2 Default policy that always fails (finds leftover string uses)
Note
Useful while refactoring: any string that hits a sink throws after logging.
trustedTypes.createPolicy("default", {
createHTML(value) {
console.warn("Refactor: string reached innerHTML", value?.slice?.(0, 80));
return null; // sink assignment throws TypeError
},
});
D. What breaks under enforcement
Note
A sink is a browser API that can turn a string into running script or unsafe HTML (for example innerHTML).
D.1 Assigning a plain string to innerHTML
Caution
With require-trusted-types-for 'script' enforced and no usable default policy, this throws TypeError.
const userInput = "<p>I might be XSS</p>";
const element = document.querySelector("#container");
element.innerHTML = userInput; // TypeError when Trusted Types are enforced and no default policy fixes it
D.2 Building a <script> via DOM nodes (still security-sensitive)
You never assigned to innerHTML, but you can still end up running script. The browser may apply Trusted Types when the script is about to run:
const untrustedString =
"console.log('A potentially malicious script from an untrusted source!');";
const textNode = document.createTextNode(untrustedString);
const script = document.createElement("script");
script.appendChild(textNode);
document.body.appendChild(script); // under enforcement: may need TrustedScript or a default policy
E. Safe ways to put HTML on the page
Tip
You usually want one clear pattern per place in your code: either the browser’s Sanitizer path, DOM construction without HTML strings, your own Trusted Types policy, or DOMPurify returning a trusted value.
flowchart TD
start([Where to start])
start --> q1{Need HTML from a string?}
q1 -->|No| e2[E.2 — DOM APIs]
q1 -->|Yes| q2{Sanitizer API fits this call site?}
q2 -->|Yes| e1[E.1 — setHTML]
q2 -->|No — using innerHTML under Trusted Types| q3{Where does sanitization live?}
q3 -->|Inside createPolicy| e3[E.3 — policy + innerHTML]
q3 -->|Library returns TrustedHTML| e4[E.4 — DOMPurify]
| # | Approach | Idea in one line | Good when… |
|---|---|---|---|
| E.1 | setHTML() |
Browser parses HTML and drops unsafe parts | You can rely on the HTML Sanitizer API; fits Perfect Types (trusted-types 'none') |
| E.2 | DOM APIs | Build nodes with createElement, textContent, … |
You do not need a chunk of HTML as text |
| E.3 | Policy + innerHTML |
You wrap strings in policy.createHTML(...) |
You use Trusted Types + CSP and want full control in one policy |
| E.4 | DOMPurify | Library outputs a TrustedHTML you assign directly |
DOMPurify is already your sanitizer |
E.1 element.setHTML(...) — browser-owned sanitizing
Good when you have an HTML string from an untrusted source. The HTML Sanitizer API is Baseline in current Chromium, Firefox, Safari, and Edge — use a recent release:
const untrustedString = "abc <script>alert(1)</script> def";
const target = document.getElementById("target");
target.setHTML(untrustedString);
Tip
This path does not use your trustedTypes.createPolicy code—the browser runs its own safe rules. List of safe methods and how this interacts with Trusted Types: F.1–F.3.
E.2 Build the DOM without an HTML string
Often the safest option when you only need structure:
el.textContent = "";
const img = document.createElement("img");
img.src = "xyz.jpg";
el.appendChild(img);
Warning
Avoid assigning raw HTML strings when you can build structure with the DOM instead.
el.innerHTML = "<img src=xyz.jpg>";
E.3 Your policy + innerHTML
Same steps as B.1. Use this when you are not using setHTML() but you do use innerHTML with Trusted Types:
const policy = trustedTypes.createPolicy("myPolicy", {
createHTML: (input) => DOMPurify.sanitize(input),
});
element.innerHTML = policy.createHTML(userHtml);
E.4 DOMPurify returns TrustedHTML for you
When the library is configured to output a trusted value:
import DOMPurify from "dompurify";
el.innerHTML = DOMPurify.sanitize(html, { RETURN_TRUSTED_TYPE: true });
If Trusted Types are enforced, the value must be a real TrustedHTML (or your default policy must accept the flow). E.3 keeps sanitizing rules inside your policy. E.4 keeps them inside DOMPurify—pick the style that matches how your app is structured.
F. HTML Sanitizer API & Trusted Types
Short reference for the HTML Sanitizer API.
F.1 “Safe” methods — browser always removes XSS-level danger
These APIs take an HTML string (and optionally a Sanitizer config). Unsafe tags and attributes are always removed, even if your custom config would allow them. If you pass no custom sanitizer, the browser uses its default rules (stricter than the bare minimum—for example comments and data-* are often removed).
| Method | Where it runs |
|---|---|
Element.setHTML() |
On a normal element |
ShadowRoot.setHTML() |
Inside a shadow root |
Document.parseHTML() |
Returns a new temporary Document so you can parse HTML off the live page, then copy nodes over |
Prefer these over innerHTML = string for untrusted HTML when the browser supports them.
F.2 “Unsafe” methods — you accept more risk
Warning
They follow your sanitizer config only. With no config they behave closer to permissive innerHTML. Only use them when you truly need something the safe APIs strip—and treat the result as still potentially dangerous, just narrower than raw HTML.
This naming echoes a pattern front-end stacks have used for a long time: opt-in APIs whose names scream “higher risk” so they stand out in code review—think React’s dangerouslySetInnerHTML, or Angular’s explicit sanitizer bypasses (bypassSecurityTrustHtml, …). A decade or so later, browsers adopted the same idea with setHTMLUnsafe / parseHTMLUnsafe: the safe path is the default; the Unsafe variant is deliberate.
| Method | Where it runs |
|---|---|
Element.setHTMLUnsafe() |
On a normal element |
ShadowRoot.setHTMLUnsafe() |
Inside a shadow root |
Document.parseHTMLUnsafe() |
Returns a Document from your string with your rules |
F.3 Using Sanitizer API and Trusted Types together
Both help prevent DOM XSS, but they work differently:
| Trusted Types | Sanitizer “safe” methods | |
|---|---|---|
| Idea | CSP tells the browser: on certain APIs, only accept trusted values (or send strings through a default policy). You write the policy code (or call a library from it). | The browser parses your HTML and removes dangerous pieces before insert. You do not go through createPolicy for the baseline XSS cuts. |
| Who decides “safe enough”? | You (your policy / sanitizer you call). | The browser for the built-in safe path (you can still pass extra sanitizer options). |
flowchart LR
subgraph TT["Trusted Types path"]
direction TB
s1[Plain string] --> pol[Policy callbacks]
pol --> tv[TrustedHTML / TrustedScript / TrustedScriptURL]
tv --> sk[Sinks: innerHTML, eval, script.src, ...]
end
subgraph SA["Safe Sanitizer path"]
direction TB
s2[Plain string] --> sh[setHTML / safe parseHTML]
sh --> tree[DOM updated by browser rules]
end
When both exist on the page:
setHTML()/ safeparseHTML()— The dangerous parts are removed inside the browser before insert. That is separate from “createTrustedHTMLin my policy and assign it toinnerHTML”.setHTMLUnsafe()/ …` — Can still be dangerous depending on config. They can take either a string or a trusted value. If Trusted Types apply, your Trusted Types step runs first, then the sanitizer.- CSP still wins overall. Example: with
trusted-types 'none'(“Perfect Types”) you cannot mint policies; safe Sanitizer methods are how you still get vetted HTML in. - Using both is fine: Trusted Types can lock down old patterns (
innerHTML, …), while safe Sanitizer calls give a built-in HTML path that always does baseline XSS cleanup.
Always removed on “safe” methods (short list): script, iframe, object, embed, …, and event handler attributes like onclick. See the HTML Sanitizer API docs for the full list.
F.4 Document.parseHTML() — parse in a temp document, then copy nodes
Useful with Perfect Types: you get a separate document, pick nodes, attach with appendChild / cloneNode:
const html = "<p>Hello</p><script>bad()<" + "/script>";
const parsed = Document.parseHTML(html);
const safeBit = parsed.querySelector("p");
if (safeBit) {
document.body.appendChild(safeBit.cloneNode(true));
}
For current browser support, see Document.parseHTML().
F.5 setHTMLUnsafe — only if you really need it
Note
Where “custom config” lives: setHTMLUnsafe(input, options) is documented on MDN as Element.setHTMLUnsafe(). The optional options.sanitizer may be a live Sanitizer instance, a plain SanitizerConfig dictionary (allow / remove lists for elements, attributes, comments, data-*, …), or the string "default" for the browser’s built-in safe baseline. For a Sanitizer object, start from new Sanitizer(options) and optionally tune with allowAttribute(), removeAttribute(), allowElement(), removeElement(), and related methods. This cheatsheet does not duplicate the full matrix—Unsafe plus custom lists is a code-review hotspot, so use MDN (and the default configuration page linked from there) as the canonical reference for what your rules still permit.
Same API family as setHTML() in E.1. Example: you widen the config (still risky):
const sanitizer = new Sanitizer();
sanitizer.allowAttribute("onblur");
someElement.setHTMLUnsafe(untrustedString, { sanitizer });
Allow-list elements but still “unsafe” API:
const sanitizer = new Sanitizer({ elements: ["p", "b", "div"] });
someElement.setHTMLUnsafe(untrustedString, { sanitizer });
Prefer E.1 setHTML() unless you have a rare need for Unsafe. How that interacts with Trusted Types: F.3.
G. Seeing violations in the browser
Tip
Use these patterns while debugging or to forward reports to your backend during testing.
const observer = new ReportingObserver((reports) => {
for (const report of reports) {
if (report.type !== "csp-violation") continue;
if (report.body.effectiveDirective !== "require-trusted-types-for") continue;
console.warn("Trusted Types violation:", report.body);
}
}, { buffered: true });
observer.observe();
Simpler catch-all for any CSP violation:
document.addEventListener("securitypolicyviolation", (e) => {
console.error(e.violatedDirective, e.blockedURI, e.sample);
});
H. Tiny “polyfill” for old browsers (tinyfill)
Caution
A few lines so trustedTypes.createPolicy exists when the browser has no native API. This does not add real CSP enforcement—it only helps your code run and still call your sanitizer.
if (typeof trustedTypes === "undefined") {
globalThis.trustedTypes = { createPolicy: (_name, rules) => rules };
}
Important
Always test with real Trusted Types enforcement in a current browser too. Longer explanation: POLYFILL.md.
Links & Resources
Official documentation
| Topic | Link |
|---|---|
W3C Trusted Types (spec, incl. pre-navigation / javascript:) |
w3c.github.io/trusted-types |
| MDN — Trusted Types API | developer.mozilla.org |
| MDN — HTML Sanitizer API | developer.mozilla.org |
| MDN — Default sanitizer configuration | developer.mozilla.org |
MDN — Sanitizer constructor |
developer.mozilla.org |
MDN — SanitizerConfig |
developer.mozilla.org |
MDN — Element.setHTMLUnsafe() |
developer.mozilla.org |
MDN — Document.parseHTML() |
developer.mozilla.org |
| web.dev — Trusted Types | web.dev |
Frederik Braun — Perfect types with setHTML() |
frederikbraun.de |
Other files in this repository
POLYFILL.md— polyfill variants, npm, CDN script examplesplayground/—index.htmlhighlights A.3setHTML()+ Perfect Types vs vulnerable patterns; policy lab covers A.2myPolicy; seeplayground/README.md(demos neednode playground/serve.mjs; they are not hosted on the static cheatsheet site)