TLDR: Use require-trusted-types-for 'script'; trusted-types 'none'; in your
CSP and nothing besides setHTML() works, essentially removing all DOM-XSS risks.
Background: Sanitizer API
I was guest at the ShopTalkShow Podcast
to talk about setHTML() and the HTML Sanitizer API. Feel free to listen to the
whole episode, if you want to take it all in. It's a great introduction.
Meanwhile, MDN has a good explanation of the Sanitizer
constructor to create a custom configuration and
Element.setHTML()
as the main entry point of the API.
People who do not want to directly insert into the document can also create a
new document with
Document.parseHTML().
Trusted Types
Trusted Types (TT) is a feature in
Content-Security-Policy
(CSP),
that can help prevent DOM-based XSS. By enabling "trusted types" in your CSP,
with a policy like so require-trusted-types-for 'script'; trusted-types 'mypolicy',
scripts will
not be able to start HTML parsing/insertion from normal strings (e.g.,
through document.write(), innerHTML= and so on).
These so-called HTML parsing sinks will now only accept TrustedHTML
objects - hence the name trusted.
Creating these types is ideally controlled with a so-called
TrustedTypePolicy
that is also allowed per the header (mypolicy in the example above).
I would argue that the creation and maintenance of policy code will require constant vigilance and maintenance by security-minded people. It is possible, but not something I would require of every web developer.
Perfect Types
This leads us to Perfect Types. The idea of so-called Perfect Types is that no policy is allowed. The following header both requires that the legacy HTML parsing APIs all require Trusted Types but also that no policy exists to ever create them:
require-trusted-types-for 'script'; trusted-types 'none';.
With this policy, your only way to safe HTML modification is by using the new safe methods:
- Use
setHTML()to directly insert into your document - Use
Document.parseHTML()to insert into a temporary document, select your desired piece of context (e.g., withquerySelector) and then usecloneNodeor otherNodeAPIs to move the elements into your current document.
Acknowledgements
This idea is heavily inspired by a great idea from
Jun Kokatsu and his
blog post about "Perfect Types".
He didn't suggest using setHTML(), because it didn't exist yet. Instead, he
described using Perfect Types to disallow the DOM XSS sinks and then relied on
React to take care of the safe HTML modification.