This is the third 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!
Previously, we discussed how at Dropbox we have deployed CSP at scale to protect against injection attacks. First, we discussed how we extract signal from violation reports to help create a host whitelist and restrict the sources of code running in our application. We also discussed how nonce sources allow us to mitigate XSS attacks due to content injections. Unfortunately, our CSP policy still has 'unsafe-eval'. This allows the use of string → code constructs like eval, new Function, setTimeout (with a string argument), and thus leaves an XSS risk open.
Needless to say, this is not great. Unfortunately, due to wide use of legacy JS templates in our client-side code, it is not easy to disable eval. While we migrate away from these legacy templates to React, we were also wondering what the exact risk from including unsafe-eval in a page's CSP policy is, and how to mitigate it.
At first glance, unsafe-eval does not seem like a terribly insecure directive. Unsafe eval only controls whether the browser allows 'eval' (and its variants like new Function); but if an attacker is able to call eval, the attacker has already achieved code execution and we have lost. An exploit would require the attacker to inject into strings that then flow into an eval "sink." This is in contrast to unsafe-inline, which allows an attacker to convert a simple HTML injection vulnerability into a code injection vulnerability.
Unfortunately, on further investigation, we realized that this simple reasoning was not true. The main reason for this was our use of libraries such as jQuery and (on legacy pages) Prototype. In fact, the presence of unsafe-eval, while using jQuery or Prototype, negated the advantages of removing 'unsafe-inline' from our policy. Lets dive into more detail for jQuery, but note that similar bugs exist in Prototype and possibly other libraries too.
Consider the following two lines of HTML. At a glance, it seems like they will achieve the same result:
document.getElementById("notify").innerHTML = untrusted_input
jQuery("#notify").html(untrusted_input)
With a CSP policy that disallows inline scripts, untrusted_input can have all the onclicks in the world, the browser will not execute them. This is true for both lines of code. But, if untrusted_input contains an inline script tag (e.g., alert(1)), the two lines of code diverge.
In the first case, innerHTML does not support inline script tags and the alert will fail to execute. In the second case, jQuery will parse out the script tag, realize that directly setting untrusted_input via innerHTML won’t work. Instead, jQuery will parse out the contents of the script tag and directly eval the code inside the script tag. Worse, if untrusted_input is https://attacker.com/foo.js then jQuery will XHR that foo.js file and eval it (thus, even bypassing the content source restrictions on scripts). The code to do this is in the core domManip function in jQuery. The jQuery code calls this function for nearly all dom manipulation operations (insert, append, html, etc.)
Another example of this is the jQuery.ajax function. At a glance, this function looks like a simple function to make XHR requests. Unfortunately, jQuery, by design, provides extra powers to its ajax function. In particular, if the response of an XHR request has the content-type script, jQuery will eval the response (Github issue). This means that any place where the attacker is able to control the target URI of an ajax call becomes a code injection vulnerability.
In the presence of a CSP policy disallowing eval, the browser would block both these cases. Unfortunately, enabling such a policy was an expensive option for us. Instead, to mitigate this risk, we implemented a "security patch" on top of jQuery that blocks these unsafe behaviors. We are now happy to share our jQuery patches for securing against such 'unexpected evals'. We hope that the broader community finds these patches useful. And, if you find other places needing a patch, please share them with us!
There are two key components of the patch. First, we remove the implicit eval in ajax with the line below that replaces the default handler for script responses (set here in the jQuery code to an eval) with a no-op:
jQuery.ajaxSettings.converters["text script"] = true
Second, we override the default domManip with our own implementation that checks the script tag for the presence of the right nonce value before executing it. The patch just reimplements the domManip function (copied verbatim from jQuery) but the key patch is in line 183 of the domManip function:
// line 181:
for (i = 0; i < hasScripts; i++) {
node = scripts[i];
if ((window.CSP_SCRIPT_NONCE != null) &&
(window.CSP_SCRIPT_NONCE !== node.getAttribute('nonce')) {
console.error("Refused to execute script because CSP_SCRIPT_NONCE" +
" is defined and the nonce doesn't match.");
continue;
}
Another option is to just disable this behavior outright by deleting these lines or use something like jPurify that sanitizes all jQuery DOM operations. The important point here, though, is that if you are deploying a CSP policy with ‘unsafe-eval’, it is important to mitigate this risk in some manner to protect against XSS attacks.
Supporting trusted eval uses
As I mentioned earlier, we cannot remove unsafe-eval from our policy because our legacy code still requires the use of unsafe eval. In particular, we need unsafe-eval because of our use of JavaScript Microtemplates. The template library essentially evals (using new Function) the template (stored in a script tag marked with a content-type of ('text/template'). For example, here’s a template from John’s original blogpost:
<script type="text/html" id="user_tmpl">
<% for ( var i = 0; i < users.length; i++ ) { %>
<li><a href="<%=users[i].url%>"><%=users[i].name%></a></li>
<% } %>
</script>
The templating code looks up the template using the id parameter and then calls (in essence) new Function on the contents of the script tag above. Unfortunately, this also means that an attacker can exploit an HTML injection vulnerability to insert a malicious template that is eval’ed by our template library.
This is not great. To mitigate this risk, we inserted nonce attributes in all template script tags and modified the template library to also check the nonce attribute of template nodes. This is similar to how the browser checks the nonce attribute for script nodes.
<script id=test type=text/template nonce=1234>
...// template library only processes this if
...// window.CSP_SCRIPT_NONCE equals 1234
</script>
<script type=text/template>
...//the templating library will ignore this
</script>
One issue we hit is that sometimes our client-side code downloads the template after the page load. Since the server-side generates a new nonce each time, the nonce on the template downloaded after page load would be different from the nonce for the main page. To mitigate this issue, we modified our code at the server side. Instead of creating a random nonce on each load, the script nonce for our pages is the hash of the CSRF tokens (note that the CSRF tokens are already random, unguessable values). This reduces the security of the nonce to the security of the CSRF token, but if an attacker knows the CSRF token, they can already take arbitrary actions for the user via CSRF attacks.
Finally, this is another reminder that CSP is a mitigation and defense-in-depth feature and is not intended to be the first line of defense. The correct defense for XSS is to construct HTML securely in a framework that automatically escapes untrusted data followed by a good DOM sanitizer as a second layer of defense.
Thanks to all the Dropboxers who helped explain the intricacies of the Dropbox website to me. Special thanks to David Goldstein who implemented the fix to jQuery.ajax. Up next, we will talk about issues with CSP and third-party integrations as well as risks associated with them.