[CSP] Unsafe-inline and nonce deployment

This is the second of four posts on our experience deploying Content Security Policy at Dropbox. If this sort of work interests you, we are hiring! We will also be at AppSec USA this week. Come say hi!

In the previous post, we discussed how to filter reports and deploy content source whitelists using CSP for the website. Typically, the most important content sources to whitelist are the source of your code, as defined by the script-src (and the object-src directive). A standard content-security-policy deployment will typically include a list of allowed domains like the main website and trusted CDNs in script-src as well as directives like 'unsafe-inline' and 'unsafe-eval'.

While this policy prevents arbitrary inclusion of third party scripts, this does not provide protection against XSS attacks due to the presence of the 'unsafe-inline' directive. If you are not familiar with the 'unsafe-inline' and nonce-src directives, please take a look at the CSP primer on html5rocks; but here’s a quick recap: by default, CSP blocks all inline script blocks ( tags) and inline event handlers (<div onclick="somescript">). This enforces code-data separation: all code running in the page has to come from script files in a whitelist of sources. This significantly reduce XSS risk, but it is difficult without a huge migration effort.

As a way to ease migration, the specification accepts a special source syntax for the script-src and style-src directives that allows inline content if it has a matching nonce attribute. Thus, a policy with nonce-randomnumber in the source list for script-src will allow script tags that have "randomnumber" as the value of their nonce attribute. The nonce syntax is part of CSP2 and is currently only supported by Chrome and Firefox. On other browsers, using inline script tags requires enabling all inline scripts via 'unsafe-inline'.

This post will discuss our experience deploying nonces on the Dropbox website. Before we go into details, let me stress that CSP is only a mitigation and is not a replacement for robust validation and sanitization. At Dropbox our preferred libraries are Pyxl and React at the server and client side respectively, while we use DOMPurify for client-side HTML sanitization.

Deploying Script Nonces

Deploying script nonces involves two steps: including the right nonce with all inline script tags and removing all inline event handlers. At Dropbox, we use Pyxl for server-side HTML generation. Pyxl converts HTML tags into Python objects and automatically escapes untrusted data during serialization. For inserting script nonces, we modified the Pyxl serialization code to insert the right nonce element into the script tag.

The next step, removing inline event handlers, is difficult. For a while now, we have deprecated the use of inline event handlers and new code written at Dropbox does not use them. But this still left us with a big chunk of old code that had not been migrated. To ease the transition, we decided to automatically rewrite inline event handlers to use inline script tags instead. Essentially, the code <div id="foo” onload="somescript"> gets converted to:

<div id="foo"><!-- if id is absent, we create a unique id -->
  // The nonce in the script tag above will be 
  // inserted during Pyxl serialization
      var e = document.getElementById(foo);
      e.addEventListener("load", function(ev){
  //  ...somescript.. 

A few things stand out in the example above. First, we use an immediately invoked function expression to not pollute the global namespace with our modifications. Second, we insert a script tag that adds this onload event handlers right after the original tag is opened, instead of after the original div element ends or after the DOMContentLoaded event. While the latter two are probably fine, the behavior above closely matches the browser’s original behavior, and thus, is apt for an automatic transform.

If you are wondering: yes, this transformation does not fix existing XSS attacks in the onload code and, thus, isn’t a completely secure transform. However, we deemed this an acceptable risk because systems like Pyxl have gotten reasonably good at identifying and preventing XSS in our server-side code. Further, over time, we plan to deprecate all inline event handlers which should take away this risk.

With this change, only inline scripts and event handlers we know about will execute. If an attacker manages to insert an event handler due to, say, a DOMXSS, browsers supporting the nonce attribute will block it.

Handling inline script violations reports

Like all CSP deployments, the typical way to roll out the new nonce attribute is to roll it out in report-only mode. At Dropbox, we were in report-only mode with the nonce for nearly a month. Similar to violation reports for content sources, it is important to filter out noise even for inline script violations.

In addition to the tricks specified in the previous post, Chrome sends two additional fields to help understand an inline script violation: script sample and source file. The former is extremely valuable to quickly grep through the code base, in case reproducing the issue locally is difficult. The latter points to the source JavaScript file that inserted the inline script via DOM APIs. During the filtering phase for inline script violations, we filter out all reports where the source file field is a URI that does not belong to our application (ad injectors and extensions are a common source of violations). Similarly, per Neil’s excellent advice, we also filter out reports with script samples containing code clearly not from our application (e.g., scripts that include the string "lastPass").

Firefox currently has had a bug where even if a nonce src allows execution of inline script, Firefox reports a violation, while still executing the code. As a result, to reduce noise, I recommend, for now, deleting the report-uri for Firefox if you are deploying nonce support.
Update: This bug was fixed! But, the fix will go live in Firefox 43, scheduled for release in Dec 2015, so I still suggest not sending report-uri for Firefox till late January, 2016.

With the proper filtering in place, we deployed our policy with nonce sources and started a process of looking for violations and fixing them. This is a tedious process particularly at our scale and large code base. But, the rewards are worth it and progress is measurable. After a couple of weeks, we reached a place where we were comfortable deploying nonce-src in enforcement mode.

Mitigating DOMXSS even without nonce support

Unfortunately, nonce sources are a feature of CSP2. While Chrome and Firefox have supported this for a while, Safari and Edge do not. Both browsers ignore the nonce source syntax and enforce the 'unsafe-inline' source expression instead. To further harden our web application, we relied on another trick:

document.addEventListener('DOMContentLoaded', function () {
    var metaTag = document.createElement('meta');
    metaTag.setAttribute('http-equiv', 'Content-Security-Policy');
    metaTag.setAttribute('content', "script-src https: 'unsafe-eval';");

Just to recap: the DOMContentLoaded event is fired after the browser executes all the HTML and synchronous scripts (including inline scripts). After that, any other JavaScript tasks, events queued up, or other onload handlers fire. Following performance best practices, we already do not execute remote scripts synchronously, so the vast majority of our JavaScript code executes after DOMContentLoaded.

The code outlined above inserts a second CSP policy after the DOMContentLoaded event fires. Importantly, the second CSP policy does not include ‘unsafe-inline’. Browsers handle multiple CSP policies by enforcing all of them, so only code permitted by all is executed. The net result of this is that Safari and Edge parse and support inline script only in the initial HTML until the DOMContentLoaded event fires. After that, these browsers stop supporting inline event handlers.

Imagine that an attacker is able to insert a malicious payload (<div onclick=alert(1)>) via, say, innerHTML. In Chrome and Firefox, the presence of nonces in the original policy delivered via a header blocks the inline onclick. In Safari and Edge, while the first policy allows the onclick handler, the second policy, inserted after the DOMContentLoaded event, blocks it. While this is a weaker protection than on browsers supporting nonce sources, it does protect against a large class of DOMXSS attacks.

Final Thoughts

We have found the techniques outlined above to be an effective mitigation against XSS attacks in our web application. Between Chrome, Firefox, Safari, and Edge, a huge chunk of our users have a strong mitigation in place. While we fix all injection vulnerabilities, it is a welcome relief to know that even successful injection attempts have a second barrier in place for the vast majority of our users.

Deploying something as major as CSP, particularly deprecating 'unsafe-inline' requires support from the whole company. Special thanks to all the Dropboxers involved in this project, particularly all the members of the security engineering team. This is the second of a series of blog posts detailing our experience deploying CSP. Up next, we will talk about the impact of including ‘unsafe-eval’ in our policy and how to mitigate the risk.