Help build the future of open source observability software Open positions

Check out the open source projects we support Downloads

We cannot remember your choice unless you click the consent notice at the bottom.

Trusted Types: How we mitigate XSS threats in Grafana 10

Trusted Types: How we mitigate XSS threats in Grafana 10

2023-07-11 9 min

Grafana is a rich platform for data visualization, giving you full control over how your data should be visualized. However, this flexibility and freedom comes with some challenges from a security perspective — challenges that need to be solved to protect the data in Grafana.

For years, cross-site scripting (XSS) has been among the most common web application security vulnerabilities. One of the main reasons for this is simple — XSS is a hard problem to solve, especially when the application expects user-supplied data, and even HTML sometimes. Grafana has some history with XSS vulnerabilities, which is to be expected since it supports so many third-party plugins, some of which allow the use of arbitrary HTML in panels. 

In the past, we’ve mitigated XSS vulnerabilities in Grafana by having a strict Content Security Policy (CSP) and strictly sanitizing HTML, but one type of XSS could still bypass these protections: DOM XSS. 

To address this, we’ve implemented Trusted Types as an experimental feature for self-hosted instances of Grafana 10. This blog post will go through what Trusted Types are, why we decided to adopt them, and how we’ve implemented them in Grafana. We’ll also walk through some of the challenges we faced and provide some tips in case you’re interested in doing something similar for your security framework.

What is DOM XSS?

A DOM XSS vulnerability is a type of vulnerability that occurs in a website’s JavaScript code and is therefore in the DOM, and not on the server-side.

DOM XSS occurs inside a JavaScript context when the script takes a string from a controllable “source” and feeds it to a “sink” that allows dynamic code execution, such as eval(), innerHTML, location.href, and document.write().

What is the Trusted Types directive?

The Trusted Types directive is an experimental JavaScript API, initially developed by Google, that tries to combat DOM XSS vulnerabilities from occurring in your web application. It’s a browser-based feature we added support for in Grafana 10. You can enable Trusted Types by adding the directive require-trusted-types-for ‘script’ to the CSP. This will instruct the web browser that certain DOM APIs require a “trusted type” object, and not a raw string.

Trusted Types has defined a set of unsafe DOM APIs that are often the root cause for introduced DOM XSS vulnerabilities. Examples of these unsafe DOM APIs include eval, innerHTML, document.write, and ScriptElement.src, among others. 

The Trusted Types directive consists of two concepts — types and policies. These concepts make two things possible:

  1. Verification that a raw string into an injection sink has been explicitly handled.
  2. Input not sanitized by the developer may be implicitly sanitized.

A policy is an object you create that is responsible for sanitization. When a string is run through a policy, it returns a Trusted Type. This type can only be created with policies. When trusted types are enabled in the Content Security Policy, an error is shown when an injection sink receives something other than a Trusted Type. This forces developers to actively sanitize their code.

JavaScript
const policy = trustedTypes.createPolicy('policy-name', {
  createHTML: (input) => sanitize(input), //Sanitize the string and return a trusted type
});

// Will return <img src=”” </img> 
div.innerHTML = policy.createHTML(“<img src= onerror=alert(1) /></img>”);

For cases where input has not been explicitly sanitized, we can apply a default policy. This policy does explicit sanitization, instead of crashing or throwing errors. However, the risk with using a default policy that’s too harsh is that your application may unexpectedly break.

Trusted Types currently has three interfaces that injection sinks may use: TrustedHTML, TrustedScript, and TrustedScriptUrl. We won’t go into deeper details on how each of these work, but in summary:

  • The TrustedHTML interface is used to create trusted HTML content. 
  • The TrustedScript interface is used to create trusted JavaScript code.
  • The TrustedScriptURL interface is used to create trusted URLs that can be used to load JavaScript code. 

It is important to mention that Trusted Types don’t do any explicit sanitizing, nor do they provide any recommendations on how to do so. Instead of using a traditional approach of finding places where a non-sanitized string is passed to a dangerous sink, Trusted Types policies work more as a “hook” and let you decide what to do each time a string is passed to a sink — preferably sanitizing.   

How we implemented Trusted Types in Grafana

Trusted Types have shown to be a suitable approach to protect against DOM XSS in Grafana. We decided to partly follow the W3C’s recommendation on how to become Trusted Types compliant. Instead of refactoring our own code, we created dedicated policies that perform proper sanitization to not throw Trusted Types violations. We also created a default policy for third-party code (libraries and plugins).

For third-party code, having a defined, default policy allows us to sanitize strings passed to dangerous sinks without the plugin maintainers needing to change their code. However, even though that’s the best approach, the sanitization might still interfere with the functionality of the plugin. On top of that, testing hundreds of Grafana plugins would be very time consuming, so we need a perfect balance with HTML sanitization; if we sanitize too strictly, the plugin won’t function, and if our sanitization is too lax, we might not get the full potential of Trusted Types.

Conveniently, DOMPurify, which is the default HTML sanitizer used in Grafana, has support for Trusted Types through the RETURN_TRUSTED_TYPE config option. DOMPurify also handles feature testing, so if a browser does not support Trusted Types, a string would be returned instead, which is the normal behavior.

In general, we noticed that the Trusted Types violations were not verbose enough, which made us decide to handle the logging ourselves. We did that by defining a default policy that does more verbose logging when CSP is used in report-only mode. When CSP is used in blocking mode, we do the actual sanitization. Here’s a picture and some pseudo code that hopefully explains the implementation better:  

A flowchart shows how different dirty strings are handled.

There are three alternatives for sanitization: 1. Don’t do it. 2. Do sanitization, but without any mechanism to verify that it has been sanitized. 3. Do sanitization and verify using Trusted Types.

The third case works as follows for a string without explicit Trusted Type sanitization:

  1. Take either a dirty string or a string sanitized without Trusted Types.
  2. If Content-Security-Policy-Report-Only is used:
  • Do no sanitization, insert string as-is.
  • Log the Trusted Types violation together with source, sink, and string in the console.
  1. If Content-Security-Policy is used:
  • Apply the default policy sanitization.
  • Insert sanitized string.

For the case where a string has been explicitly sanitized with Trusted Types, it is inserted directly into the injection sink.

The approach on sanitization 

Now that you know how we implemented Trusted Types, there’s one thing left: how we sanitize the strings in the default policy.

TrustedHTML

TrustedHTML is an interface that can create HTML from a string. Grafana has support for “strict CSP,” which means that it utilizes strict-dynamic together with nonces in the script-src directive that should only allow “non-parser-inserted” script elements.

Because of this, it made sense to sanitize only these script elements in the default policy and let the CSP block other types of script execution, e.g. via event handlers, meaning we do the following:

JavaScript
createHTML: (string) => { return string.replace(/<script/gi, '&lt;script') } 

TrustedScriptURL

TrustedScriptURL is an interface that can create a URL of a script resource, for example <script src=URL. Usually, it would make sense to only allow HTTP/S, but since we already have a library that sanitizes URLs, we decided to utilize that instead. This library will remove schemas such as data:, javascript:, and vbscript: from a URL.

TrustedScript

TrustedScript is an interface that can execute code represented from a string when passed into an injection sink. For the default policy, we do not currently do any sanitization. We found it challenging to sanitize arbitrary JavaScript without knowing if it’s harmless or not. 

Measuring the efficiency of Trusted Types in Grafana

When implementing Trusted Types, we wanted to find a way to verify that our implementation was sufficient. One method we used was to disable all HTML-sanitization, which basically makes Grafana vulnerable to XSS by design. Then we enabled CSP and Trusted Types to see if XSS was still possible by creating a Text Panel injected with various XSS Payloads.

Below, you’ll see that event handlers are blocked by the standard CSP, but script tags are being executed.

A screenshot shows the event handlers blocked but the script tags are still being executed.

When Trusted Types is active, the script tag gets encoded and does not execute the JavaScript. 

This screenshot shows the script tag gets encoded and doesn't execute the JavaScript.

A second method we used was to measure how effective the implementation was. We also tested against historical vulnerabilities to ensure our implementation was effective. By applying CSP and Trusted Types, we did not manage to exploit any historical XSS vulnerabilities

A chart shows how Trusted Types blocked all historical XXS vulnerabilities, including thoes not blocked by CSP.

(Note: For the JavaScript links related to Grafana OnCall and Grafana Incident, you can find the issues that were opened here and here, respectively.)

Challenges

There are some limitations to Typescript support for Trusted Types. Although there is a package available for them (@types/trusted-types), Typescript will throw an error when trying to insert a Trusted Type into an injection sink. This is because the injection sinks are typed to receive strings. A common workaround for this is to typecast the Trusted Type into a string, by writing as unknown as string. Unfortunately, this is considered a bad practice and defeats the purpose of Typescript.

Instead we managed to re-type some injection sinks on a global level, to take the input string | TrustedHTML, for example. There are still more injection sinks to be typed there, and there is some exploration into an upstream contribution to @types/trusted-types.

Another challenge was how we should sanitize using the default policy. Since Grafana supports plugins, we wanted to utilize a default policy in order to sanitize inputs in plugins. We started using DOMPurify for createHTML, but since some plugins use AngularJS, it was difficult to build a safe DOMPurify policy that would allow custom elements and attributes. AngularJS is deprecated in Grafana, and it’s expected to be removed in the foreseeable future. Once it has been removed from the code base, we may consider trying a stricter sanitization in the hopes of mitigating more vulnerabilities.

Since W3C does not recommend how to sanitize (which is intentional as it’s out-of-scope for the project), it was difficult to understand how to generically sanitize strings using the TrustedScript interface, since the default policy is applied to Grafana plugins and contains many unknowns. Finding a way to sandbox/isolate JavaScript would be an ideal solution to how we should handle code in the TrustedScript interface.

Final thoughts and the future

Trusted Types have shown to be an effective means of combating DOM XSS in Grafana. Together with a strict CSP, we can greatly reduce the attack surface. Although neither our implementation nor Trusted Types can fully mitigate DOM XSS due to bypasses and lack of browser support, strict CSP and Trusted Types can be part of the solution.

Looking ahead, we want to further test the implementation and improve where possible. If you want to try this out in Grafana, feel free to leave us some feedback!