Skip to content

Effects of deploying Trusted Types on browser extension developers

Krzysztof Kotowicz edited this page Dec 28, 2021 · 3 revisions

This document details the changes that enforcing Trusted Types on a website brings and describes how browser extension developers might be affected by the changes. It also discusses recommended refactorings to make sure your browser extension remains compatible with a website that enforces Trusted Types.

Background

Trusted Types is a browser security feature that limits access to dangerous DOM APIs to protect against DOM-based Cross-Site Scripting (DOM XSS). Trusted Types provide type guarantees to all frontend code by enforcing security type checks directly in the web browser. They are delivered through a Content Security Policy (CSP) header and have a report-only mode that does not change application behavior and an enforcement mode that may cause user-observable breakages.

When enforced, Trusted Types block dangerous injection sinks (such as .innerHTML) from being called with values that have not passed through a Trusted Types policy. Using injection sinks with plain string values results in a violation and will be blocked by the browser.

What can change?

Once Trusted Types are enforced in a web application, you will see the Trusted Types directive in the CSP header:

Content-Security-Policy: require-trusted-types-for 'script';

This will instruct the browser to block any action that causes a Trusted Types violation. A Trusted Types violation occurs when an untrusted value (typically a string) is passed to an injection sink.

You can preview these changes by adding this header when visiting a web application yourself. You can add headers either by using extensions such as ModHeader or using an intercepting proxy (e.g. Burp or OWASP ZAP) to intercept traffic and modify headers.

What might break?

Websites enforcing Trusted Types typically have already refactored all their JavaScript code not to cause Trusted Types violations, such that the enforcement does not cause functional breakages. That said, Trusted Types restrictions also affect the code that browser extensions inject into the web page’s isolated world. For that code, any use of the injection sinks such as:

  • Script manipulation
    <script src> and setting text content of <script> elements.
  • Generating HTML from a string:
    innerHTML, outerHTML, insertAdjacentHTML, <iframe srcdoc>, document.write, document.writeln, and DOMParser.parseFromString
  • Executing plugin content:
    <embed src>, <object data> and <object codebase>
  • Runtime JavaScript code compilation:
    eval, setTimeout, setInterval, new Function()

may result in a functional breakage of your browser extension, since the extension violates the restrictions that the website set in its CSP. When this happens, you will see an error message similar to the following in the browser console:

Trusted Types violation

How can I prevent breakages?

First, you can make sure that the code causing the violations is not subject to Trusted Types restrictions by making the DOM operations in the content script isolated world. If this cannot be done, there are a couple of ways to refactor code to be compliant with Trusted Types.

Trusted Types require you to process the data before passing it to the risky injection sink functions. To make your code Trusted Types compliant you need to signal to the browser that the data you’re using within the context of these risky functions is trustworthy by creating a special object - a Trusted Type.

You can remove the offending code, use a library, create a Trusted Types policy, or as a last resort create a default policy (not recommended!).

Move DOM operations to the content script isolated world

Content script isolated worlds are not affected by Trusted Types headers set by the web page. This means that in many cases, your extension may work around Trusted Types restrictions by performing DOM operations directly in the content script isolated world, instead of injecting the scripts that perform DOM changes in the web page’s isolated world.

Rewrite the offending code

In some cases the DOM operations are executed in the the web page's isolated world - these will be affected if the website starts enforcing Trusted Types with the CSP header. Sometimes the risky functionality of the injection sink you’re using might not be needed, and you can refactor the offending code by using a function that is not an injection sink.

For example if you’re using innerHTML to change the text content, without needing the HTML rendering capabilities:

el.innerHTML = 'new text content'

You can use:

el.textContent = 'new text content'

You can also rewrite examples such as:

el.innerHTML = '<img src=xyz.jpg>';

To avoid using error-prone functions:

el.textContent = '';
const img = document.createElement('img');
img.src = 'xyz.jpg';
el.appendChild(img);

Use a library

Many libraries already generate Trusted Types that you can pass to the sink functions.

DOMPurify

For example, you can use DOMPurify to sanitize an HTML snippet, removing XSS payloads.

import DOMPurify from 'dompurify';
el.innerHTML = DOMPurify.sanitize(html, {RETURN_TRUSTED_TYPE: true});

Safevalues

You can also use the safevalues to create Trusted Types. For example, to convert a string into a safely HTML-escaped TrustedHTML:

import {htmlEscape} from 'safevalues';

const trustedHtml = htmlEscape('<img src=a onerror="javascript:alert()">');
// TrustedHTML{'&lt;img src=a onerror=&quot;javascript:alert()&quot;&gt'}

Both safevalues and DOMPurify support Trusted Types and will return HTML wrapped in a TrustedHTML object such that the browser does not generate a violation when this value is passed to an injection sink like .innerHTML.

Create a Trusted Types policy

When it’s not possible to rewrite the offending code without using risky injection sink functions, and there is no library to sanitize the value you need, you can create your custom Trusted Type objects. To do this you will first need to create a policy.

// Note: use self.trustedTypes when the code is running inside a Web Worker.
if (window.trustedTypes && trustedTypes.createPolicy) { // Feature testing
  const escapeHTMLPolicy = trustedTypes.createPolicy('myEscapePolicy', {
    createHTML: string => string.replace(/\</g, '&lt;')
  });
}

This code creates a policy called myEscapePolicy that can produce TrustedHTML objects via its createHTML() function. The defined rules will HTML-escape < characters to prevent the creation of new HTML elements.

You can use the policy like so:

const escaped = escapeHTMLPolicy.createHTML('<img src=x onerror=alert(1)>');
console.log(escaped instanceof TrustedHTML);  // true
el.innerHTML = escaped;  // '&lt;img src=x onerror=alert(1)>'

Use a default policy [Not Recommended]

When you can’t change the offending code, (this might be the case if you’re loading a third-party library from a CDN), you can use a default policy:

if (window.trustedTypes && trustedTypes.createPolicy) { // Feature testing
  trustedTypes.createPolicy('default', {
    createHTML: (string, sink) => DOMPurify.sanitize(string, {RETURN_TRUSTED_TYPE: true})
  });
}

The policy with a name default is used wherever a string is used in a sink that only accepts Trusted Type.

Warning: Trusted Types forbid creation of muiltiple default policies in a single document. We do not recommend using it in your browser extensions, because there will be a race condition, and your policy creation might fail if other extensions attempt to create it as well. The logic depending on the default policy may unpredictably fail.