setHTML(), Trusted Types and the Sanitizer API
Browser support note
Chrome shipped and then un-shipped an early version of the Sanitizer API. Avoid older resources about this API as the spec has changed over time.
The Sanitizer API is supported in Firefox Nightly, in line with the latest specification. It is available in Chrome Canary behind a flag. While Safari has not started implementation work, the Safari team do have a positive position on the API.
The Trusted Types API is supported in Chrome/Edge, Samsung Internet, Safari, and Firefox Nightly. Chrome has supported Trusted Types since version 83 but version 144 brings it fully in line with other browsers and the latest specification.
setHTMLUnsafe is supported in all browsers.
setHTML()
The setHTML() method inserts HTML into the DOM in a way that prevents cross-site scripting attacks.
const div = document.querySelector('div');
div.setHTML(html);
setHTML() will always filter out inline event handlers and the following HTML elements:
scriptembedframeiframeobject- The SVG
useelement
By default, it also removes far more: style, link, img, video, button, form, input, textarea, label, select, option, output, details, summary, template and all custom elements (e.g. <wa-dropdown>), data attributes, aria attributes, inline styles and HTML comments (this is not a fully comprehensive list and it looks like it could potentially change in the future).
Given the following code:
const html =
`<h1 onclick="console.log('hi')">testing</h1>
<button>Click me</button>
<img src="cat.jpg">
<iframe src="https://olliewilliams.xyz/">`;
const div = document.querySelector('div');
div.setHTML(html);
The contents of div will be the following:
<h1>testing</h1>
Configuring what gets stripped
Which elements and attributes get removed can be configured via a custom sanitizer. For the most permissive approach, the following code will remove only embed, frame, iframe, object, script, and use elements, and inline event handlers:
div.setHTML(html, {sanitizer: {}});
Whitelist
When using an allowlist configuration, you define which elements and attributes are permitted. Anything not listed will be removed.
const mySanitizer = new Sanitizer({
elements: ["h1"],
attributes: ["style"]
});
div.setHTML(html, {sanitizer: mySanitizer});
Alternatively an object can be used:
div.setHTML(html, {
elements: ["h1"],
attributes: ["style"]
});
If you need fine-grained control, allowed attributes can be specified on an element-by-element basis:
const sanitizer = new Sanitizer({
elements: [
{ name: "h1", attributes: [] },
{ name: "h2", attributes: ["style"] },
],
});
The above sanitizer will strip all attributes from h1’s, but will not strip the style attribute from h2’s.
Blacklist
Alternatively, you can configure the sanitizer using a blocklist approach by specifying which elements and attributes should be removed:
const sanitizer = new Sanitizer({
removeElements: ["a"],
removeAttributes: ["id"],
});
Given the above sanitizer, setHTML(html, {sanitizer: sanitizer}) will only remove a, script, embed, frame, iframe, object and use elements, id attributes and inline event handlers.
Trusted Types and the Sanitizer API
Trusted Types and the Sanitizer API share a common goal: preventing cross-site scripting (XSS). The two APIs complement each other: while the Sanitizer API provides a safe way to construct DOM trees, the Trusted Types API enforces that only sanitized content can be passed into unsafe DOM sinks, ensuring no developer can accidentally introduce XSS vulnerabilities into your codebase.
You opt in to Trusted Types via the Content-Security-Policy (CSP) header.
Content-Security-Policy: trusted-types passthrough legacysanitize; require-trusted-types-for 'script';
Other directives that aren’t related to Trusted Types are also typically included in the CSP header, but I’m keeping the above example minimal. passthrough and legacysanitize are the names of policies I wish to create on the frontend — you can name them whatever you want and list as many as you need. What you list here restricts which policy names can be used in client-side code.
If the above response header is set, whenever a string is passed to an unsafe sink it will cause a TypeError e.g.:
div1.setHTMLUnsafe(`<iframe src='https://olliewilliams.xyz/'/>`); // TypeError
div2.innerHTML = "<h2>Hello world</h2>"; // TypeError
An error such as the following will be displayed in the browser devtools console:
What counts as an unsafe sink?
Two modern examples:
setHTMLUnsafe()Document.parseHTMLUnsafe()
Older examples include:
innerHTMLouterHTMLparseFromString()document.writedocument.writeln- Setting the
srcdocproperty of aniframeusing JavaScript
You can still use these methods when the trusted types header is set, but not with strings. Instead, you must work with TrustedHTML objects.
Creating a policy
A TrustedHTML object is returned by the createHTML method. How the string is transformed is up to you to define — the Trusted Types API itself does not sanitize the string or do anything else to make it safer. The below example takes the input HTML and returns it unchanged — but as a TrustedHTML object rather than a string:
const passThroughPolicy = trustedTypes.createPolicy("passthrough", {
createHTML: (input) => input
});
const unsanitizedHTML = passThroughPolicy.createHTML(`<iframe src='https://olliewilliams.xyz/'/>);
The above code is effectively saying “I trust this HTML”. Obviously this isn’t a great general policy! Passing this unsanitized HTML to innerHTML or any other sink won’t result in an error, but it’s no safer than working with a regular string. There might be some cases where it’s necessary to inject HTML without sanitization in this way (the setHTMLUnsafe() method exists for a reason). Fetching HTML from the server that includes declarative shadow DOM, for example:
const target = document.getElementById('target');
fetch('/hopefullyverytrustworthy')
.then(response => response.text())
.then((html) => {
const unsanitizedHTML = passThroughPolicy.createHTML(html);
target.setHTMLUnsafe(unsanitizedHTML);
});
In general though, createHTML is used to sanitize the string. Browser support for Trusted Types is broader than support for the Sanitizer API, so a policy will typically sanitize the string using an open source library such as DOMPurify.
window.trustedTypes.createPolicy('legacysanitize', {
createHTML: (input) =>
DOMPurify.sanitize(input, { RETURN_TRUSTED_TYPE: false }),
});
(RETURN_TRUSTED_TYPE: false is required because DOMPurify.sanitize can return a TrustedHTML object directly but createHTML expects a string.)
Once browser support for the Sanitizer API improves, there will be no need for third party tools like DOMPurify. The createHTML boilerplate will be avoidable in most cases as you can simply use setHTML() instead. The setHTML() and Document.parseHTML() are not unsafe sinks and will not cause an error when Trusted Types is enabled. You pass them strings, not TrustedHTML objects.
When possible you should:
- use
setHTML()overinnerHTMLandsetHTMLUnsafe() - use
Document.parseHTML()overparseFromString()orDocument.parseHTMLUnsafe()
There may be some cases where setHTML() can’t be used, such as when setting the srcdoc of an iframe, for example.
This is all pretty new. If I’ve made any mistakes please let me know via Bluesky, Twitter, or Mastodon.