Didn't make the #ChromeDevSummit this year? Catch all the content (and more!) in the Chrome Dev Summit 2019 playlist on our Chrome Developers YouTube Channel.

Trusted Types help prevent Cross-Site Scripting

TL;DR

We've created a new experimental API that aims to prevent DOM-Based Cross Site Scripting in modern web applications.

Cross-Site Scripting

Cross-Site Scripting (XSS) is the most prevalent vulnerability affecting web applications. We see this reflected both in our own data, and throughout the industry. Practice shows that maintaining an XSS-free application is still a difficult challenge, especially if the application is complex. While solutions for preventing server-side XSS are well known, DOM-based Cross-Site Scripting (DOM XSS) is a growing problem. For example, in Google's Vulnerability Reward Program DOM XSS is already the most common variant.

Why is that? We think it's caused by two separate issues:

XSS is easy to introduce

DOM XSS occurs when one of injection sinks in DOM or other browser APIs is called with user-controlled data. For example, consider this snippet that intends to load a stylesheet for a given UI template the application uses:

const templateId = location.hash.match(/tplid=([^;&]*)/)[1];
// ...
document.head.innerHTML += `<link rel="stylesheet" href="./templates/${templateId}/style.css">`

This code introduces DOM XSS by linking the attacker-controlled source (location.hash) with the injection sink (innerHTML). The attacker can exploit this bug by tricking their victim into visiting the following URL:

https://example.com#tplid="><img src=x onerror=alert(1)>

It's easy to make this mistake in code, especially if the code changes often. For example, maybe templateId was once generated and validated on the server, so this value used to be trustworthy? When assigning to innerHTML, all we know is that the value is a string, but should it be trusted? Where does it really come from?

Additionally, the problem is not limited to just innerHTML. In a typical browser environment, there are over 60 sink functions or properties that require this caution. The DOM API is insecure by default and requires special treatment to prevent XSS.

XSS is difficult to detect

The code above is just an example, so it's trivial to see the bug. In practice, the sources and the sinks are often accessed in completely different application parts. The data from the source is passed around, and eventually reaches the sink. There are some functions that sanitize and verify the data. But was the right function called?

Looking at the source code alone, it's difficult to know if it introduces a DOM XSS. It's not enough to grep the .js files for sensitive patterns. For one, the sensitive functions are often used through various wrappers and real-world vulnerabilities look more like this.

Sometimes it's not even possible to tell if a codebase is vulnerable by only looking at it.

obj[prop] = templateID

If obj points to the Location object, and prop value is "href", this is very likely a DOM XSS, but one can only find that out when executing the code. As any part of your application can potentially hit a DOM sink, all of the code should undergo a manual security review to be sure - and the reviewer has to be extra careful to spot the bug. That's unlikely to happen.

Trusted Types

Trusted Types is the new browser API that might help address the above problems at the root cause - and in practice help obliterate DOM XSS.

Trusted Types allow you to lock down the dangerous injection sinks - they stop being insecure by default, and cannot be called with strings. You can enable this enforcement by setting a special value in the Content Security Policy HTTP response header:

Content-Security-Policy: trusted-types *

Then, in the document you can no longer use strings with the injection sinks:

const templateId = location.hash.match(/tplid=([^;&]*)/)[1];
// typeof templateId == "string"
document.head.innerHTML += templateId // Throws a TypeError.

To interact with those functions, you create special typed objects - Trusted Types. Those objects can be created only by certain functions in your application called Trusted Type Policies. The exemplary code "fixed" with Trusted Types would look like this:

const templatePolicy = TrustedTypes.createPolicy('template', {
  createHTML: (templateId) => {
    const tpl = templateId;
    if (/^[0-9a-z-]$/.test(tpl)) {
      return `<link rel="stylesheet" href="./templates/${tpl}/style.css">`;
    }
    throw new TypeError();
  }
});

const html = templatePolicy.createHTML(location.hash.match(/tplid=([^;&]*)/)[1]);
// html instanceof TrustedHTML
document.head.innerHTML += html;

Here, we create a template policy that verifies the passed template ID parameter and creates the resulting HTML. The policy object create* function calls into a respective user-defined function, and wraps the result in a Trusted Type object. In this case, templatePolicy.createHTML calls the provided templateId validation function, and returns a TrustedHTML with the <link ...> snippet. The browser allows TrustedHTML to be used with an injection sink that expects HTML - like innerHTML.

It might seem that the only improvement is in adding the following check:

if (/^[0-9a-z-]$/.test(tpl)) { /* allow the tplId */ }

Indeed, this line is necessary to fix XSS. However, the real change is more profound. With Trusted Types enforcement, the only code that could introduce a DOM XSS vulnerability is the code of the policies. No other code can produce a value that the sink functions accept. As such, only the policies need to be reviewed for security issues. In our example, it doesn't really matter where the templateId value comes from, as the policy makes sure it's correctly validated first - the output of this particular policy does not introduce XSS.

Limiting policies

Did you notice the * value that we used in the Content-Security-Policy header? It indicates that the application can create an arbitrary number of policies, provided each of them has a unique name. If applications can freely create a large number of policies, preventing DOM XSS in practice would be difficult.

However, we can further limit this by specifying a whitelist of policy names like so:

Content-Security-Policy: trusted-types template

This assures that only a single policy with a name template can be created. That policy is then easy to identify in source code and can be effectively reviewed. With this, we can be certain that the application is free from DOM XSS. Nice job!

In practice, modern web applications need only a small number of policies. The rule of thumb is to create a policy where the client-side code produces HTML or URLs - in script loaders, HTML templating libraries or HTML sanitizers. All the numerous dependencies that do not interact with the DOM, do not need the policies. Trusted Types assures that they can't be the cause of the XSS.

Get started

This is just a short overview of the API. We are working on providing more code examples, guides and documentation on how to migrate applications to Trusted Types. We feel this is the right moment for the web developer community to start experimenting with it.

To get this new behavior on your site, you need to be signed up for the "Trusted Types" Origin Trial (in Chrome 73 through 78). If you just want to try it out locally, starting from Chrome 73, the experiment can be enabled on the command line:

chrome --enable-blink-features=TrustedDOMTypes

or

chrome --enable-experimental-web-platform-features

Alternatively, visit chrome://flags/#enable-experimental-web-platform-features and enable the feature. All of those options enable the feature globally in Chrome for the current session.

We have also created a polyfill that enables you to test Trusted Types in other browsers.

As always, let us know what you think. You can reach us on the trusted-types Google group or file issues on GitHub.

Was this page helpful?
Yes
What was the best thing about this page?
It helped me complete my goal(s)
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
It had the information I needed
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
It had accurate information
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
It was easy to read
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
Something else
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
No
What was the worst thing about this page?
It didn't help me complete my goal(s)
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
It was missing information I needed
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
It had inaccurate information
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
It was hard to read
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.
Something else
Thank you for the feedback. If you have specific ideas on how to improve this page, please create an issue.

rss_feed Subscribe to our RSS or Atom feed and get the latest updates in your favorite feed reader!