Web Security Training

Navigating the web security landscape

Navigating the web security landscape

Article

The new way of doing CSP takes the pain away

Did you know that 95% of CSP policies can easily be bypassed? This shocking revelation came from research done by Google, and the culprit are overly optimistic whitelists. That’s the bad news. The good news is that CSP Level 3 comes with a new way of dynamically loading scripts, which fixes these problems, and makes CSP a lot easier to use! Find out what it all means in this article.

Content Security Policy is a new browser-based security mechanism, which evolves very quickly. Ever since CSP Level 3 started supporting trust propagation, I’ve been following the story closely, and have even talked about the awesomeness of it to a room full of Angular developers, with great feedback afterwards.

When I started the blog post series on CSP, I had no idea that I would end up here, having to amend a lot of things I said before. However, the feedback on strict-dynamic is mainly positive, and other browsers are starting to offer support as well. Therefor, this makes for a nice conclusion of this series on CSP. Read more about CSP in the previous posts in this series:

Of course, I will keep reporting on new developments when they appear, either in additional blog posts or by adding them to the websec digest. If you have subscribed to yet, go ahead and do that now!

Whitelisting is a waste of time

Generally, building a CSP policy requires you to whitelist a lot of resources. If you take at the previous posts about the CSP policy of this very website, you’ll notice that I have to whitelist a lot of stuff. For a simple website like this, I have to add different hosts from Twitter, Google and Disqus, just to enable some basic widgets, as shown below …

script-src 
    'self'
    https://ajax.googleapis.com/ajax/libs/webfont/1.5.18/webfont.js 
    https://www.google-analytics.com/ https://platform.twitter.com/ 
    https://cdn.syndication.twimg.com 
    https://syndication.twitter.com 
    https://websec-be.disqus.com 
    https://*.disquscdn.com

Unfortunately, it seems that all the effort you put into carefully constructing your whitelist was a waste of time. Last summer, research from Google revealed that 95% of CSP policies currently in place were trivial to bypass. The reason? Insecure whitelists! And not just those whitelists that simply allow scripts from everywhere, but whitelists built by people that understand security and CSP. In fact, the whitelist that this website used, as shown above, is vulnerable to a bypass attack. I know, shame on me!

Most CSP policies are trivial to bypass. Have you checked your policy lately?


So how is this possible? Let’s see what Google’s CSP Evaluator has to say about the policy shown above.

Google’s CSP Evaluator shows that whitelisting the entire Twitter CDN is a bad idea

It turns out that by whitelisting Twitter’s CDNs for the widget, we actually whitelist a lot of other content to be included. This content may include JSONP endpoints, meaning that an attacker would be able to inject a script tag that loads such a resource, where the actual callback code is under control of the attacker. A similar problem exists when you are able to load a whitelisted Angular library from a CDN, as that can be used to render innocent-looking text into executable script.

I’m not going into all the details of these bypasses here. If you are interested in learning more about these bypass attacks, take a look at the slides or video of the original AppSec EU talk covering this work.

Fixing broken whitelists

As most whitelists can be bypassed, you might think that CSP is broken beyond repair. But that’s not true. There are two concrete things you can do to fix the problems with whitelists.

The first solution is to actually whitelist up to the file level. If you only whitelist explicit script files on a CDN, there’s no way to include potentially dangerous scripts, such as an Angular library of a script coming from a JSONP endpoint. There is however one caveat: in most scenarios, whitelisting explicit files is next to impossible to get right, and to maintain.

That’s exactly what Google find out as well. They tried to use CSP level 2 with whitelisting, and concluded it just didn’t work for them. Which is why they proposed a new way of handling scripts, using the strict-dynamic keyword in combination with nonces. This would result in a policy like this one:

script-src ‘nonce-$randomvalue’ ‘strict-dynamic’

Wow, if that’s all you need, strict-dynamic must be magical! It’s not, but it is pretty awesome. Let’s break down what the policy above actually means. The first part defines a nonce that can be used to include inline and remote scripts. This is part of the CSP Level 2 specification, and has been around for quite some time.

In case you need a refresher, the nonce in the policy must match a nonce defined on a script block or remote script, and must be freshly generated for each page. This allows you to whitelist scripts you have put in the page yourself, but prevents an injected script from being executed, as shown below.

<meta http-equiv="Content-Security-Policy" content="script-src 'nonce-Saik14t9sjq24'">

<script nonce="Saik14t9sjq24">
// This will be loaded, because the nonce matches
</script>

<p> Hello Philippe<script>alert("No nonce, CSP blocks me :(")</script></p>

The second part of the policy is the strict-dynamic keyword. This is where the magic happens. When you add strict-dynamic to the policy, the browser is instructed to trust scripts that are allowed to run (by using nonces), and extend that trust to additional scripts that are loaded by already trusted scripts. Sounds complex, but let me take you through an example:

  • If you want to include a Twitter timeline, you include a snippet of code you get from Twitter. You inherently trust this piece of code by putting it in your page
  • That code will load additional files from Twitter, behavior you actually trust as well (since you have put this in your page). Normally, you would have needed to whitelist these files, but with strict-dynamic, the browser propagates trust and simply loads the file.
  • The newly loaded Twitter script needs to load even more resources (Hey, welcome to the web!), which would again need to be whitelisted. But just as before, the browser allows this to happen, because of strict-dynamic.

And this is the true beauty of strict-dynamic, it simply propagates trust that was already there. If you still need to be convinced, take a minute to actually think this through. Before CSP, the code would have been able to do whatever it wanted. With CSP, you would have whitelisted the code anyway (see the original policy of this website), even though you did not really know what it does. So while strict-dynamic seems like an enormous leap of faith, it simply applies the same implicit process you used before.

CSP's new 'strict-dynamic' makes me want to give it a try!


A final word about strict-dynamic: It only works for non-parser-inserted scripts. This means that it only works for script elements that are being added to the page by using DOM APIs, not by triggering the parser (e.g. through innerHTML). This is to ensure that the developer actually intended to add script elements to the page, instead of becoming the victim of an injection attack through a text-to-code sink.

For more information about the reasoning behind strict-dynamic, I can highly recommend the AppSec EU talk, where strict-dynamic was first introduced to the world (slides, video).

Towards a universal CSP policy

As I said before, CSP Level 2 just didn’t work for Google, but strict-dynamic actually does. They even propose a universal CSP policy that does not require any whitelisting, and will actually work for just about any application that follows decent coding standards (e.g. does not mix JavaScript with HTML). The policy looks like this:

Content-Security-Policy:
    object-src 'none';
    script-src 'nonce-$random' 'strict-dynamnic' 'unsafe-inline' 'unsafe-eval' https: http:
    report-uri https://yourreportingendpoint...

That can’t be right, it includes unsafe-inline, and whitelists all http and https resources!!! Well yes …, but it kind of depends on the browser. And it’s only there for backwards compatibility, not for security. But it’s a mess. Take a look at the picture below, which should clarify things a bit.

The universal CSP policy only offers decent protection on modern browsers, but does not break anything on older browsers.

Let’s break it down case by case. The best case scenario is when the user has a browser that supports strict-dynamic. In that case, the following rules apply:

  • Using nonces means that the browser will ignore unsafe-inline, so inline scripts are blocked
  • Using strict-dynamic means that the browser will ignore whitelists (e.g. http: and https:), so only nonced scripts will be able to load resources
  • The unsafe-eval is there to ensure absolute compatibility with all applications, but can be removed if your application does not need it

For browsers that do not support strict-dynamic, the universal CSP policy ensures that it does not break the application. Let’s see what happens if the browser supports CSP Level 2, but not strict-dynamic:

  • Using nonces means that the browser will ignore unsafe-inline, so inline scripts are blocked
  • strict-dynamic will be ignored, but the whitelist ensures that remote scripts will be loaded anyway (albeit without any protection against malicious script inclusions)
  • The unsafe-eval is there to ensure absolute compatibility with all applications, but can be removed if your application does not need it

Finally, for browsers that are still at CSP Level 1, and do not support nonces, the following rules apply:

  • The nonce will be ignored, but unsafe-inline ensures that everything keeps working. Obviously, there is no protection against injected script blocks.
  • strict-dynamic will be ignored, but the whitelist ensures that remote scripts will be loaded anyway (albeit without any protection against malicious script inclusions)
  • The unsafe-eval is there to ensure absolute compatibility with all applications, but can be removed if your application does not need it

The policy proposed here works for almost every application. Of course, you are still free to narrow down the whitelist if you prefer. In fact, that is how Google deployed this policy for their Photo application, as you can see below

Google deploys CSP with strict-dynamic for a lot of its services.

A word about object-src

As mentioned in the introduction, I have updated the policy of this site to use strict-dynamic. During that update, I actually followed the advice of the CSP Evaluator, and set object-src to none. The main reason to do so, is that this site does not use plugins such as Flash and Java.

However, from inspecting the reports, it became apparent that this was also blocking Chrome’s built-in PDF viewer, even for displaying full-page PDFs. While this may seem weird, it’s actually the way its documented in the HTML spec. What really happens when you open a PDF is that Chrome creates a new HTML page, inserts an embed tag to load the PDF, and renders that page. In doing so, it applies the CSP policy that is being served with the PDF, which caused the viewer to be blocked.

report-uri.io allowed me to detect the blocking of PDF files.

When I tweeted about this problem, it turned out almost nobody knew about this kind of behavior. Thanks to Mike West’s quick response, it has now been explicitly mentioned in the CSP spec, which will hopefully help other people detect and fix this problem as well.

There are essentially two sensible ways to address the problem:

  1. Make an exception for PDF files, and serve a different policy to allow the plugin to run
  2. Modify the site-wide policy to allow plugin content to be loaded from my own origin

The former is obviously more restrictive than the second approach, and it protects you better against content sniffing attacks (where the browser misinterprets a content type, resulting in an XSS attack vector).

For now, I have opted to go with the second approach, and have added the directive shown below to the CSP policy of this site. The main reasoning behind this decision is that the site consists of static content only, and that I control all of the content.

object-src 'self'

Using strict-dynamic with hashes

If you take a look at the CSP policy below, which is the policy currently deployed for this site, you will see a lot of hashes in there, but no nonces. This is a statically generated site, meaning that it would be hard to include a freshly generated nonce in every response from the server. Attendees for my training courses have asked me similar questions for sites running on static hosting solutions, such as Amazon S3.

Content-Security-Policy:
    default-src 'self'; 
    script-src 'unsafe-inline' 'strict-dynamic' http: https: 'sha256-1/ynuPtBacxxdexsTmOS2xYF+Y5QItZhLd0Jfh+fAXA=' 'sha256-J6t4DqIRmrkriHVxPRqY6SJvaxk1X+7peDYT4V//KyA=' 'sha256-UP3mdiNS0wyquC7Pb3ZZ3vaFDhcKpprROF0xpgMm34c=' 'sha256-77AySqlb7L8j/4ZYyDgL025JmQfiRqegSfVizVQuvd8=' 'sha256-uxzi2OgpNXaSA/8wT6X4JDqw8Y+ODCxCwB+WsuWagBY='; 
    object-src 'none'; 
    style-src 'self' 'unsafe-inline' https://fonts.googleapis.com/ https://platform.twitter.com https://*.twimg.com https://*.disquscdn.com; 
    img-src 'self' data: https://www.google-analytics.com/ https://syndication.twitter.com https://*.twimg.com https://platform.twitter.com https://referrer.disqus.com https://*.disquscdn.com; 
    frame-src https://platform.twitter.com https://syndication.twitter.com https://disqus.com/ https://player.vimeo.com https://www.youtube.com; 
    font-src 'self' https://fonts.gstatic.com; connect-src https://links.services.disqus.com; 
    report-uri https://websec.report-uri.io/r/default/csp/enforce

Instead of modifying your backend infrastructure, you can specify your CSP policy with hashes and strict-dynamic. The pages of this site load remote scripts dynamically, like the example shown below:

<script>
    function loadScript(url) {
        var s = document.createElement("script");
        s.src = url;
        document.head.appendChild(s);
    }

    loadScript("https://platform.twitter.com/widgets.js");
</script>

Because the CSP policy contains the hash of this inline script block, the browser marks it as trusted, and actually allows the script to load additional files through strict-dynamic. While this may result in a small performance penalty, it allows me to deploy CSP to protect you against injection attacks!

Where you vulnerable to these CSP bypass attacks? Or will strict-dynamic help you adopt CSP in your applications? Let me know in the comments below!

Want to stay informed?

Subscribe to our mailing list and never miss an update or an event!

BLOGPOSTS

Comments & Discussion