Eliminating XSS by adopting Trusted Types

Hi everyone.

My name is Bjarki and I'm a security engineer at Google where I work on Web framework security.

Today, I'm going to show you how you can eliminate cross site scripting vulnerabilities from your Web application by adopting a new web platform security mechanism called Trusted Types.

Let's start with a little bit of background.

Google's VRP is a bug bounty program that pays external security researchers for reporting vulnerabilities.

Looking at data from 2018 and the total payouts by vulnerability class, we can see that cross-site scripting accounts for more than a third of the entire budget.

And considering that almost half the budget isn't even on Web related issues, we can see that XSS is by far the most expensive type of Web vulnerability.

And this graph looks largely the same even today, as well as if you look at other bug bounty programs.

Cross-site scripting vulnerabilities occur when a Web application passes user input, which hasn't been escaped or sanitized into an HTML or script context, thatthe browsers and interprets and executes.

And this can be serious.

As an attacker may be able to leverage this to leak sensitive data from users or perform actions on their behalf.

This can happen both because of unsafe service side and client side rendering.

Client side or DOM based XSS, which we are going to focus on occurs when an application passes user input to one of the dangerous DOM APIs, which we call injection sinks.

On the right you can see some examples of injection sinks.

Eval is probably the most recognized one, but assigning to inner HTML, or script.src are other good examples and will cause XSS if passed untrusted user input.

There ave been a number of attempts at mitigating DOM based XSS, including content security policies, static code analysis and XSS detection in browsers.

While most of these help considerably, as we saw on the previous slide XSS is still a huge threat.

Expanding a bit on content security policies, they allow you to restrict which scripts are allowed to run on your page.

They are configured by having a web server send an HTTP header.

For example, a Content-Security-Policy header with the value of script-src https://example.com.

will make the browser block execution of any scripts that are not loaded over HTTPS from example.com.

And this ensures that only scripts that you trust are executed on your page.

While a good CSP policy may limit or even clock impact from DOM XSS in your application, it does not address the root cause of these vulnerabilities, which are unsafe uses of injection sinks.

And indeed attackers may, may still be able to deface yuor web site or use it for phishing attacks.

Luckily, most modern clientside frameworks such as Angular or React do a great job of limiting direct interaction with injection sinks.

They provide a templating language that ensures that all interpolated user input is escape or sanitized by default, before rendering.

If you need to bypass some decision, you need to be explicit about doing so.

In react, this is achieved by using the dangerouslySetInnerHTML property and in Angular, you must call one of the bypass security trust functions, which marks the value as safe for injection into a specific context.

But even if a secure web framework is used, there are still many opportunities for direct DOM interaction that could lead to XSS.

For example, there's nothing that forces a developer to use the frameworks templating language.

So they may still call out to the DOM directly.

And in some cases, this may even be necessary.

External jealous with libraries that the application depends on are also a common source of accessible our abilities as they frequently perform direct DOM interaction.

And both of these points are especially true in large applications where there's often little oversight over what code gets checked in.

And this brings us to the main subject, which is Trusted Types.

Trusted Types is a new Web browser API that allows you to completely block write access to injection sinks.

They are a CSP feature and are configured by serving a CSP header with require-trusted-types-for 'script'.

When enabled any attempt at using an injection sink will be blocked by the browser.

Throwing an exception, also known as a Trusted Types violation.

You can verify that this works by opening the developer console in your application and executing eval on a plain string which will generate an error message saying that this document requires trusted script assignment.

Trusted Types is supported in Chromium-based browsers as of version 83, which includes Chrome at Opera and Brave.

But Trusted Types are backwards compatible for the most part, other browsers would just ignore the CSP directive and thus do not provide the same level of defense.

On the other hand, you will need to use a web framework and libraries that are compatible with Trusted Types, meaning that they do not cost any Trusted Types violations.

There are already a number of libraries and frameworkthat have been made compatible with trust types, including Angular, Polymer, DOMPurify, CodeMirror as well as the next major release of jQuery.

Of course, if the libraries is only using safe DOM APIs, it will work out of the box with Trusted Types.

React can also be made compatible with Trusted Types by compiling with the enableTrustedTypesIntegration flag.

And the same for Webpack with the output.trustedTypes option.

Enabling Trusted Types makes an application very secure, but it can also break its functionality.

So before rolling this out to production, we first need to identify the Trusted Types violations and fix them.

There are a few different ways to identify Trusted Types violations.

The simplest way is to configure your local development.

To send the CSP header.

And then open the application in a browser.

Trust Types violations will surface as exceptions in the developer console and contain a stack trace.

The same can be done for the server that serves end to end tests for your application.

Running end tio end tests with Trusted Types enabled is a good source of violations, if the tests are comprehensive.

It is also possible to use static code analysis.

We've developed a tool called tsec, that looks for Trusted Types violations in TypeScript code.

Finally, you can use a feature of CSP known as report only mode, which allows you to collect, collect and analyze CSP reports containing Trusted Types of violations from actual users without causing production breakage.

This is a good practice before deploying any CSP feature.

Now, when you've identified the usage of an injection sink that is causing you a Trusted Types violation, you need to refactor that code in a way that does not cause a violation.

First and foremost, you should be using your web framework's templating language whenever possible.

Otherwise, here are some common examples of violations and safe alternatives.

If you are assigning plain text to innerHTML, now you can assign the text to text Content instead.

If you're constructing some simple inline HTML, you can use safe DOM APIs instead.

For example, document.createElement setAttribute, textContent, and appendChild.

If you're creating an inline eventhandlers using strings, you should use addEventListener instead.

If you're creating Web workers and you're using Webpack, you can use import.metadata.url to instruct Weback to inline the worker script in a way that is compatible with Trusted Types.

These are some common examples, but sometimes there's simply no safe alternative.

As a last resort, Trusted Types provide some mechanism known as Trusted Types Policies.

These policies allow the application to produce values that are safe for use and specific injection sinks without causing a Trusted Types violation.

Sinks require one of three different types of values depending on the nature of the sink-either TrustedHTML, for example, when assigning to innerHTML.

TrustedScript, for example, when calling eval.

Or TrustedScriptURL, for example, when creating a worker.

Policies are simple JavaScript objects defined by the application.

On the right is an example of an application that wants to assign potentially untrusted user input to innerHTML in a safe way.

It starts by defining a trusted type of policy using the trustedTypes.createPolicy API, and giving it a name of sanitize-html.

The createHTML function defines how unsafe user input should be transformed or validated to ensure that the resulting HTML is trusted.

In this case, the DOMPurify library is used to sanitize the HTML.

The application can then pass untrusted HTML to the policy's createHTML function which sanitizes the HTML and returns the trusted HTML value.

This value can then be assigned to innerHTML without causing a Trusted Types violation.

In this way, Trusted Types forces any security sensitive code into Trusted Types policies as a result, these policies must be scrutinized for security.

Once all the violations have been fixed, Trusted Types can be enabled in production.

Going forward, we must also ensure that no new Trusted Types violations appear.

Our stuff may break the application.

The approach is similar to how we originally identified the violations.

By enabling trust the types in local development mode, developers should catch any new violations when they are developing a feature.

The same applies for end to end tests, which should be run as part of your CI pipeline.

tsec, our static code analysis tool cant be added as a pre-submit check to your code repository.

And in the unlikely case, any violations make it into production, you will be notified through CSP reports.

One final feature of Trusted Types that I want to tell you about is restricting policy creation.

This is useful as a when you roll out trust the types trust, types of policies will remain as the only place where security sensitive code lives in your application.

Policy creation can be restricted by explicitly listing the names of the allowed policies in the trusted-types directive in your CSP header.

In the example, on the slide, only the two policies named sanitizeHTML and Angular are allowed.

Trying to create a policy with any other name or using the same name more than once will result in a Trusted Types violation.

Restricting policy creation gives you even stronger guarantees about the security of your application.

And it's especially useful in large applications where you may not want developers to create arbitrary, Trusted Types policies.

Some libraries may themselves create Trusted Types policies, which you can either allow or block using the trusted-types directive.

As an example, this is the case for Angular.

The Angular and Angular bundler policies are required for basic Angular functionality.

And they're always safe to allow.

The Angular unsafe-bypass policy is created with any of the bypass security trust functions are called.

Depending on whether you allow this policy, you can control whether you want to allow calls to bypass security trust functions in your application.

And if you want to use the Angular dev tools while developing your application, you need to allow the Angular devtools policy, but make sure you only do this locally.

Some libraries take a different approach.

Instead of creating Trusted Types policies, they ask the user to provide trusted type values.

This is the case for React and jQuery.

But you will need to pass in a trusted HTML value when you see React's dangerouslySetInnerHTML property or jQuery's HTML function to ensure that no Trusted Types violation is generated Wrapping up: these are the steps you need in order to deploy, Trusted Types in your application.

You start by enabling Trusted Types in report only mode.

You look for Trusted Types violations either locally by navigating around your application.

Or by looking at CSP reports from users.

You fix these violations by refactoring code to use Safe DOM APIs, using Trusted Types policies as a last resort.

When you're confident you have fix all Trusted Types violations, drop the report only mode and start enforcing Trusted Types.

Finally, you can rely on end-to-end tests and continuous integration to prevent any new violations.

To get an idea of how effective trusted types are, we analyzed DOM based XSS reports from from last year's Google VRP program and found that at least 61% of the report vulnerabilities would have been mitigated by trusted types.

And note that this only includes vulnerabilities that were already missed by Google static code analysis pipeline, and security reviews, which is pretty impressive.

So we've seen how trusted types forces you to use safe APIs.

And move security sensitive code into trusted types policies that can be reviewed in isolation, even in browsers that do not support Trusted types, this still increases the security of your application considerably.

But trusted types gives you more than that.

I was recently playing around with one of my Angular applications, when I ran into a Trusted types violation.

I thought to myself, Oh, no, I must've forgotten to refactor some insecure code in my application, but after digging into the stack trace, I noticed that the violation was coming from inside the Angular framework.

I was using internationalization in myAndular application, and it turned out that in certain scenarios, Angular did not escape user input that went through the i18n pipeline.

I had discovered a cross site scripting vulnerability, in Angular itself, but because I was enforcing trusted types, my application was immune to the vulnerability, at least in browsers that support trusted type.

And the same principle applies to obscure, but potentially dangerous code paths in both your application and the third-party dependencies.

They will soon to be blocked.

So that's it.

Thank you for listening.

And I hope I've inspired you to give trusted types a try in your Web applications.

Eliminating XSS

by adopting Trusted Types

bjarki@google.com

Background

Google's VRP in 2018

Total payout by class

Pie chart showing 3 segments. 49.1% Non-web issues, 35.6% XSS, >1⁄2 DOM XSS

  • Mobile app vulnerabilities
  • Business logic (authorization)
  • Server/network misconfigurations
  • ...

Cross-site scripting (XSS)

  • Untrusted user input executed by browser
  • DOM-based XSS occurs in client-side APIs
  • Mitigations
    • Content Security Policy
    • Static code analysis
    • XSS detection

Script execution

eval(userInput);
new Function(userInput);

HTML rendering

el.innerHTML = userInput;
document.write(userInput);

Script loading

script.setAttribute("src", userInput);
new Worker(userInput);

Content Security Policies (CSP)

  • Restricts scripts allowed to run on your page
  • Configured through an HTTP header
    • Content-Security-Policy: script-src https://example.com
  • Limits impact of XSS

Modern web frameworks

  • Provide a secure templating language
  • Minimize direct interaction with injection sinks
  • Bypassing sanitization
    • bypassSecurityTrustHtml: SafeHtml
    • bypassSecurityTrustScript: SafeScript
    • bypassSecurityTrustResourceUrl: SafeResourceUrl
<app>
    <h1>Hello, {{name}}!</h1>
    <div [innerHTML]="userInput"></div>
</app>
<app>
  <h1>Hello, {name}!</h1>
  <div dangerouslySetInnerHTML={{
    __html: sanitize(userInput)
  }}></div>
</app>   
Gaps
  • Direct DOM interaction is still possible
  • External JavaScript libraries
  • Little oversight in large applications

Trusted Types

Trusted Types

  • Blocks unsafe writes to injection sinks
  • Configured with a CSP header
    • Content-Security-Policy: require-trusted-types-for 'script'
  • Attempts cause a Trusted Types violation

Support

  • Chromium-based browsers as of v83
  • Angular, Polymer, DOMPurify, CodeMirror, jQuery v4
  • React
    • Compiled with enableTrustedTypesIntegration flag Webpack
    • Enabled with output.trustedTypes option

Identifying violations

  • Open application in browser
  • Run end-to-end tests
  • Static code analysis (tsec)
  • Collect CSP reports (Content-Security-Policy-Report-Only)

Fixing violations

Violation

el.innerHTML = "plain text";
el.innerHTML += "<div>"+text+"</div>";
window.onload = "alert('loaded')";
new Worker('worker.js')
;

Safe alternative

el.textContent = "plain text";
const div = document.createElement("div");
div.textContent = text;
el.appendChild(div);
window.addEventListener("load", () => { alert("loaded");
});
new Worker(
  new URL('worker.js',
          import.meta.url));

Trusted Types policies

  • Produce values safe for use in injection sinks
    • TrustedHTML
    • TrustedScript
    • TrustedScriptURL
  • Defined by the application
  • Subject to security review
const sanitizeHtmlPolicy =
  trustedTypes.createPolicy("sanitize-html", {
    createHTML(unsafe: string) {
      return DOMPurify.sanitize(unsafe);
} });

const html: TrustedHTML
  = sanitizeHtmlPolicy.createHTML(userInput);
el.innerHTML = html;

Preventing regressions

  • Trusted Types in development mode
  • Trusted Types in end-to-end tests
  • Run tsec as a presubmit check
  • CSP reports as the last line of defence
Restricting policy creation
  • Policy creation can be restricted
  • Configured in CSP header
    • Content-Security-Policy:
      	require-trusted-types-for 'script';
      	trusted-types sanitize-html angular
      
  • Other policies will be blocked

Library policies

  • Libraries may create policies
  • Angular
    • angular, angular#bundler, angular#unsafe-bypass, angular#devtools
  • Trusted Types-agnostic libraries
    • React, jQuery

Conclusion

Deploying Trusted Types

  • Enable Trusted Types in report-only mode
  • Identify violations
  • Fix violations
  • Enforce Trusted Types
  • Prevent regressions with e2e tests
Empirical results
  • Analyzed DOM-based XSS reports from Google's VRP
  • At least 61% would have been mitigated by Trusted Types
  • Missed by static code analysis and security reviews
Benefits
  • Forces you to use safe APIs
  • Security-sensitive code is restricted to policies
  • Makes your application safer in all browsers
  • Provides defence-in-depth in supported browsers

Thank You