W3C Trusted Types in practical web development
Trusted Types are an emerging DOM API specification that attempt to prevent a whole range of attacks resulting from web browsers being tricked into execution of untrusted content, for example XSS.
As of April 2020 Trusted Types are in draft state but are implemented in Google Chrome as an “experimental” option, which did not prevent us from implementing it for the whole WebCookies.org. This was facilitated by the fact that the website is mostly static and JavaScript is primarily used for some trivial user interaction (menus) and API calls for scan updates etc.
TT happens fully on the browser side and doesn’t have any server-side components and its whole purpose of TT is to create a centrally managed flow for all possibly untrusted data inside browser’s DOM which will ensure untrusted data is made safe by either validation or sanitization. The following code should be part of your website’s main JavaScript library.
Using Trusted Types
I start from the end by showing the actual usage of a Trusted Types policy in JavaScript code, and explain where it came from. The thing is, whenever you actually send potentially untrusted data to a potentially risky destination such as eval()
or .innerHTML
or XMLHttpRequest
, you always wrap it in the TT policy method — and that’s really it, all about using TT in your code:
let r = new XMLHttpRequest();
r.open("GET", ttp.createScriptURL(progress_url), true);
Trusted Types policies
Now, where does that ttp.createScriptURL()
come from? The ttp
is an instance of a TrustedTypes
built-in object present in the modern browsers. The createScriptURL
is a mandatory method that you define.
First goes a no-op shim for browsers that do not support TT:
if (typeof TrustedTypes === 'undefined') TrustedTypes = {createPolicy: (name, rules) => rules};
Next we declare our primary TT policy called base
:
const ttp = TrustedTypes.createPolicy('base', {
createHTML: (input) => input,
createScriptURL: (input) => {
const url = new URL(input, document.baseURI);
// allow all same-site URLs and one allowed analytics domain
if (url.origin === window.location.origin || url.origin === "my-analytics.com") return url.href;
throw new TypeError('createScriptURL: invalid URL');
}
});
This piece of code is generally all that TT does: validate and/or sanitize potentially untrusted data of standardized types. Here we handle only “trusted HTML” type created by createHTML
method and “trusted URL” created by createScriptURL
. Only these methods in a TT-enabled browser are entitled to create these trusted objects, and in due course any risky destinations (“sinks”) will only accept these trusted objects.This is a concept well-known from other secure coding frameworks, such as safestring in Django. The idea is simple: any data that has passed through these two methods is marked as secure, and any data that hasn’t will be rejected.
Now, you might be puzzled by the fact that the createHTML
method doesn’t really do any validation here: this is because the website where I trialed it first (webcookies.org
) does not inject any user-generated content within DOM. Its threat model assumed that all calls to this method originate internally, from static HTML created by the server. If your service is processing any user-generated or external data, you want to plug in something like DOMPurify here — a secure, validating HTML parser that will only accept whitelisted HTML elements.
The createScriptURL
is simple: it only allows relative URLs (within the same origin) or URLs from trusted, whitelisted origins. Once again, this is what worked for me on that website, YMMV.
These two methods basically define what we consider as “trusted” content within the DOM on our website. As you can see, the actual concept of TT isn’t particularly complicated — as with all web security technologies, the difficult part is to register all activity that your website performs, build policies that will allow the operate in secure manner and then wrap all of them.