Trusted Types - a new way to protect web application code from XSS attacks

Google has developed an API that allows modern web applications to protect their front-end from XSS attacks, and more specifically, from JavaScript injections into DOM (DOM-Based Cross Site Scripting).

Cross-site scripting (XSS) is the most common type of attack related to the vulnerability of modern web applications. This is recognized not only by Google, but the entire industry . Experience has shown that developing a web application resistant to an XSS attack is still a non-trivial task, especially when it comes to complex projects. If the developers successfully solve this problem on the backend, then on the front-end everything is much more complicated. As part of Google's Vulnerability Reward Program more and more awards are received by developers who have proposed a solution to protect against DOM XSS attacks.

You may ask, how did we get to such a life? Probably ask. The fact is that modern web applications have two serious and unrelated problems ...

XSS is easy to embed in code


DOM XSS is a type of attack in which unfiltered data coming from the side of the user or other browser APIs, along with the implemented js injections, fall into the DOM. For example, consider a snippet designed to load a stylesheet for a certain user interface template that an application uses:

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

In this case, the script introduced by the attacker in location.hash (in this case plays the role of the data source, that is, source) will fall into innerHTML (plays the role of the data receiver, that is, sink). Using this vulnerability, an attacker could spoof the URL that the victim will go to:

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

This injection can easily be overlooked, especially if the code changes frequently. For example, once upon a time templateId could be generated and checked on the server and after that it would never occur to anyone to check it again. When we write something in innerHTML, we only know for sure that its value is a string. But should you trust the contents of this line? Where did it actually come from?

Moreover, the problem is not limited to innerHTML. In a typical DOM, there are more than 60 methods and properties that have the same problem. So, the DOM API is unsafe by default and requires a special approach to prevent XSS.

XSS is hard to track


The above code is just a good example, so it’s easy to see the error there. In practice, the values ​​of source and sink are often in completely different parts of the application. Are there any functions that perform sanitation and data verification on the way from source to sink ? But how to determine when and what function to call?

Looking only at the source code, it's hard to see if it contains DOM XSS scripts. Moreover, grep is not enough for .js files with case sensitivity settings. After all, case-sensitive functions are often used inside various wrappers, so real vulnerabilities are well disguised and may look, for example, like this .

And sometimes, even if you carefully read the entire code, it is impossible to determine if there is a vulnerability in the code base. Take a look at this expression:

obj[prop] = templateID;

If obj points to a location object (the location.hash above) and the value of prop is 'href', then this is most likely the XSS DOM. However, you can probably find out only in the process of code execution. Since any part of your application can hypothetically write something to the DOM, all code must pass a manual security check, and the reviewer must be extremely careful to detect an error. But one cannot completely rely on such a verification method, since the human factor has not been canceled.

Trusted types


Trusted Types is a new browser API that will help to fundamentally eliminate the problems listed above, including protecting web applications from the DOM XSS.

The Trusted Types API is built on a set of classes (policies) that allows you to block malicious injections. With it, the DOM ceases to be vulnerable: its elements receive values ​​not through a string, but through a special object. In order to use Trusted Types, you need to initialize a special field in the header of the Content Security Policy (CSP) HTTP response :

Content-Security-Policy: trusted-types *

CSP is an additional layer of security that can recognize and eliminate certain types of attacks, such as Cross Site Scripting (XSS) and data injection attacks. In order to enable CSP, you must configure the server so that it uses the Content-Security-Policy HTTP header in its responses.

And then no one will be able to write an arbitrary string to the element of your DOM:

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

Now for this you have to create and use special typed objects. These objects can be created in your application only with the help of specially designed functions called Trusted Type Policies.

For instance:

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 —    TrustedHTML
document.head.innerHTML += html;

Here we create a policy for the template template that checks the passed ID parameter and creates the resulting HTML. The set of create * methods calls the corresponding user-defined function and wraps the result in a Trusted Type object. For example, templatePolicy.createHTML calls the validation function for templateId and returns an object of type TrustedHTML containing (see the very first example). At the same time, the browser allows you to write data to an object of type TrustedHTML in innerHTML (which in theory expects normal HTML).

In addition to the TrustedHTML class, you can use the TrustedScript, TrustedURL, and TrustedScriptURL classes.

It might seem that the only advantage of the new API was the ability to add this check:

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

Indeed, this line is necessary to protect against XSS. But in fact, everything is much more interesting. Using Trusted Types, resolving DOM vulnerabilities boils down to properly enforcing policies. No other code checks the data from the source to the sink. Thus, only improper work with politicians can lead to security problems. In our example, it doesn’t matter where the value of templateId comes from, since the policy always first of all checks whether it passed validation. So, in the area for which a certain policy is responsible, any XSS attack will be repelled:

Uncaught TypeError: Failed to set the ’innerHTML’ property on ’Element’: This document requires `TrustedHTML` assignment.

Policy Limitations


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 that each of them has a unique name. If the application can freely create too many policies, this can lead to confusion and it will be more difficult to implement protection against the DOM XSS.

Therefore, we can limit this number by explicitly specifying a list of policy names that we will use. For instance:

Content-Security-Policy: trusted-types template

This ensures that only one policy is created with the name template. Then we can easily identify it in the source code and test its operation. Thanks to this, we can be sure that the application will be protected from the DOM XSS. And you will be happy!

In practice, modern web applications need a small number of policies. A general recommendation is to create policies where client code generates HTML or URLs — in script loaders, HTML template libraries, or in an HTML sanitizer. All of the many dependencies that are not related to the DOM do not need to use policies. And then the proper use of Trusted Types is guaranteed to protect us from XSS.

Fast start


The following is a short introduction to the API. Over time, more and more code examples, tutorials, and documentation appear on how to write and rewrite applications using Trusted Types. We believe that the web development community has long been ready to start experimenting with this.

To use the new API on your site, it must be activated directly in the browser. If you just want to run it locally, starting with Chrome 73, the feature can be enabled on the command line:

chrome --enable-blink-features=TrustedDOMTypes

or

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

If you use Google Chrome, you can enter in the address bar chrome://flags/#enable-experimental-web-platform-features. All of these options for launching the API make it active for one session.

If you experience a malfunction, use --enable-features \ u003d BlinkHeapUnifiedGarbageCollection as a workaround. See the discussion of error 929601 for more details .

There is also a polyphile that allows you to use Trusted Types in many other browsers.

You can discuss the project here . It is also available on GitHub.


All Articles