Web Security Training

Navigating the web security landscape

Navigating the web security landscape

Article

Building your First CSP Policy from Scratch

The best way to fully understand what CSP is all about is to get your feet wet. Maybe you've already had your first CSP experience. Chances are likely that you had a bad time, that you where overwhelmed by errors in the browser console, and that you gave up on CSP altogether. However, in this second post in the CSP series, I'll show you that there is light at the end of the tunnel. As I go through the CSP policy for this site, we'll talk about whitelisting resources, about a common mistake that leads to CSP bypassing attacks, and about when to use unsafe-inline.

If you take a quick look at the snippet below, you can see the full CSP policy for this site, at the time of writing. At first sight, this seems ridiculously complex, even for a simple site like this one. However, if you break it down step by step, you’ll see that it’s actually not that bad, and fairly easy to set up.

Content-Security-Policy:
	default-src 'self'; 
	style-src 'self' 'unsafe-inline' https://fonts.googleapis.com/ https://platform.twitter.com https://*.twimg.com https://*.disquscdn.com;
	font-src 'self' https://fonts.gstatic.com;
	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; 
	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; 
	connect-src https://links.services.disqus.com; 
	report-uri https://websec.report-uri.io/r/default/csp/enforce

This post is the second one in the CSP series, which I kicked off with this introductory post. Upcoming posts will dig deeper into reporting, into collisions with browser extensions, and into advanced CSP features, so stay tuned (and subscribe here if you haven’t done already)!

Breaking Everything

The very first step of enabling CSP is breaking a lot of features on your site. And that’s normal. If you take a look at the CSP header above, you’ll notice how we need to whitelist quite a lot of resources to enable certain features on the site. So you can imagine what happens when we start our very first CSP policy with the following directive:

Content-Security-Policy: 
	default-src 'self'

The browser console already gives a good indication of what the effect of this CSP policy is.

This basic CSP policy triggers a heap of errors, which may seem overwhelming to address.

This very simple directive tells the browser that all resources on this page should originate from the page’s own origin. For this site, this means that most content will be there, but that the CSP policy will block the Twitter feed, the comments below a blog post, embedded videos, analytics, … Yay.

Of course, what you don’t see is the actual purpose and tremendous power of CSP. If there is malicious code from a script injection attack present in the page somewhere, CSP will also block it. If an attacker has managed to embed third-party code that tries to install malware on your visitor’s machines, CSP will block it. And that’s exactly why CSP is so important, it puts you back in control over what happens on your site.

In the remainder of this post, we’ll see how to build up a CSP policy step by step, so that we can re-enable our desired features. I will provide you with a few examples of how I enabled CSP on this site, and how I addressed the problems I encountered.

Unsafe-inline doesn’t sound very secure

Let’s ignore all the errors generated by our third-party components for now. Re-enabling the twitter feed and Disqus commenting system is something to worry about later. We’ll start with the CSP errors that lie at the core of our site, caused by the libraries that the template we’re using depends on. If you take a look at the screenshot with the errors above, you can see that Modernizr, a feature detection library, causes quite of a few of them.

The error tells us that Modernizr is trying to inject style information directly into the page, instead of using an external CSS file. The browser helpfully proposes a few solutions to enable such behavior with CSP, such as using unsafe-inline, hashes or nonces. We’ll talk about hashes and nonces in more detail in a future post, but in a nutshell, the options here are:

  • Turn off protection by allowing inline styles
  • Whitelist a specific style block by providing a hash
  • Whitelist a specific style block by adding a nonce

Obviously, the second and third option are preferred over the first, since they uphold the restrictions enabled by CSP. Unfortunately, in the Modernizr case, the use of hashes and nonces does not address the problem. In fact, other people have also ran into this problem, and it seems that there is no easy way to make Modernizr compatible with CSP, without breaking its very core functionality. The people behind the library are however aware of the need for CSP support, so hopefully, that will arrive somewhere in the future.

Bummer. Should we just give up then? Well, we had three options, and only discarded two of them. So, what’s up with this 'unsafe-inline'? By adding the unsafe-inline option to the style-src directive, we essentially prevent our CSP policy from blocking any inline styles, even though they are a potential security risk. This means that we can use style blocks inside our HTML pages, and that we can use style attributes on HTML elements. This also means that the Modernizr library will be able to do its thing.

Adding 'unsafe-inline' to our CSP policy results in the following header:

Content-Security-Policy: 
	default-src 'self'; 
	style-src 'self' 'unsafe-inline'

Unfortunately, the Modernizr case is not a single occurrance of the dependency on inline styles. Many libraries and components actually depend on this feature, which is why you won’t find many CSP policies that do not enable 'unsafe-inline' for styles. And while inline styles do impose a certain security risk, they are definitely less dangerous than inline scripts, for which the unsafe-inline option is definitely not recommended.

Whitelisting Libraries and Components

Let’s move on to the next issue. The template we’re using uses WebFont to load fonts, which is again blocked by our minimalistic CSP policy. From the errors generated by the CSP policy, we learn that we need to whitelist the WebFont script files, and the associated sources for font files and stylesheets. This is a simply enumeration process until we have resolved the errors.

Translated to CSP directives, we now get the following policy:

Content-Security-Policy: 
	default-src 'self'; 
	style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
	font-src 'self' https://fonts.gstatic.com;
	script-src 'self' https://ajax.googleapis.com/ajax/libs/webfont/1.5.18/webfont.js

Reloading the site with the new CSP policy eliminates a few errors from the console, and resolves our font issues. Awesome.

Specific == Secure

If you take a close look at the CSP policy needed to enable font loading on the site, you see that we load a very specific version of the WebFont library. The whitelist in the script-src directive only allows one specific file to be loaded from Google’s script CDN.

We have restricted this source expression on purpose, to ensure the effectiveness of our CSP policy. We could also have used a much less restrictive value, such as https://ajax.googleapis.com, but then we would opened our page up to a CSP bypass attack.

In a CSP bypass attack, an attacker manages to exploit a cross-site scripting (XSS) vulnerability, even though CSP is enabled on the page. An example of such an attack is shown in the code snippet below.

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.1/angular.min.js"></script>"></script>

<div class="ng-app">
{{constructor.constructor('alert("I can do whatever I want at this point ...")')()}}
</div>

As you can see in the code, the attacker first loads a version of the popular AngularJS library from Google’s CDN. Remember, this is not stopped by CSP if we use the very relaxed source expression script-src https://ajax.googleapis.com. In itself, no harm has been done yet, since the attacker only loaded the AngularJS library. No malicious code has been run yet.

However, the second part of the injected code snippet shows a div element, essentially a piece of data. This data is not considered to be script code, it doesn’t mean much to our application, so it seems harmless. However, once AngularJS is loaded in the browser, it will recognize the div element as part of an AngularJS application, and will process the code between the curly brackets. Since the attacker has injected an old version of AngularJS, he can execute any piece of JS code, without any restrictions.

If your CSP policy is not specific enough, you might be vulnerable to CSP bypassing attacks!


To summarize, the attacker has abused an injection vulnerability to load AngularJS, and has used AngularJS to convert data into script. The first action is not stopped by our CSP policy, since we have implicitly whitelisted the AngularJS library. The second action is also not stopped by CSP, because the expression between curly brackets is interpreted by AngularJS, and not loaded as inline script. So the attacker has successfully bypassed our CSP policy, simply because we were not restrictive enough.

If you want to know more about this kind of attacks, I can warmly recommend the OWASP AppSec EU 2016 talk on Making CSP Great Again.

Going all the way

With the CSP policy outlined above, we have set a good baseline to enable the core functionality of our site. Enabling external components, such as the Twitter timeline or the Disqus commenting system is actually not that difficult.

To enable these components, we follow roughly the same steps as we did for enabling the font loader. You start by whitelisting the resources that are currently being blocked, and repeat this until all errors have disappeared. This works quite well for Twitter and Disqus, and results in the CSP policy shown below.

Content-Security-Policy:
	default-src 'self'; 
	style-src 'self' 'unsafe-inline' https://fonts.googleapis.com/ https://platform.twitter.com https://*.twimg.com https://*.disquscdn.com;
	font-src 'self' https://fonts.gstatic.com;
	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; 
	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; 
	connect-src https://links.services.disqus.com; 
	report-uri https://websec.report-uri.io/r/default/csp/enforce

You may notice that we have chosen to whitelist the Twitter hosts, instead of digging down to the file level. The main reason here is that Twitter loads various scripts from different locations, and that they do not offer a public CDN on those hosts. For their CDN hosting images, we use a wildcard to enable all subdomains, which may vary dynamically.

As you can see, the Disqus case is very similar. There’s however one caveat with Disqus. If you take a look at this very page, you may notice that Disqus still triggers a CSP violation, and actually requires 'unsafe-eval' to operate correctly. By default, CSP prohibits the use of the eval() function in JavaScript, to prevent a class of XSS attacks. Disabling any CSP protections for scripts is probably a bad idea, and I was quite hesitant to do so. Fortunately, it appears that Disqus does not really need the eval() functionality, and that it will be phased out at some point in the future.

Taking the big leap

With the policy outlined above, we have succeeded to eliminate all of our CSP violations. So all that’s left to do is actually enable the policy on our site, and hope that we don’t mess up. Fortunately, CSP offers a Report-Only mode, which you can use to see your policy in effect, without breaking anything.

In this mode, the browsers of your visitors will check your CSP policy, and detect any potential violations. If you specify a reporting endpoint, you will receive a report of these violations, and you can further fine-tune your policy. After a while, when you see no worrying violations anymore, you can flip the switch and enable the policy in blocking mode.

That’s exactly the process I followed for this site. In an upcoming post, I’ll talk about how to handle reporting, and what you can learn from those reports.

Now is the time to try CSP on your own applications, or even on your hobby projects (we all know you have some!). Let me know how it goes in the comments below!

BLOGPOSTS

Comments & Discussion