Experimenting with Content Security Policy on GOV.UK

The GOV.UK team recently had a firebreak to repay some technical debt, experiment with things we might not otherwise have the chance to, and prepare for how we're going to iterate the site in 2015. I looked at implementing Content Security Policy (CSP) on GOV.UK to enhance the safety of our users. Here's what I did.

What is CSP?

CSP is a W3C specification which helps website authors improve security by providing a whitelist to restrict where an HTML page can load assets from. For example, setting this HTTP header:

Content-Security-Policy: default-src 'none'; img-src;

will prevent this image in the body of an HTML document from being requested:

<img src="" />

For a site like GOV.UK which publishes a large amount of information from editors all over government, CSP will give us more confidence that our users are safe. We can use CSP to ensure that images, JavaScript and stylesheets are only loaded from domains that we control.

Reporting violations

A nice feature of CSP is that you can instruct the user's browser to send you a report whenever it encounters something that it refuses to load. In the previous example, appending report-uri /csp-report to the CSP header would cause the browser to send an HTTP POST request with a body that looks something like this:

  "csp-report": {
    "document-uri": "",
    "referrer": "",
    "blocked-uri": "",
    "violated-directive": "img-src",
    "original-policy": "default-src 'none'; img-src; report-uri /csp-report"

This improves the ease of debugging your CSP rules once they're deployed, because it's possible to receive information on how the policy is performing by setting up an endpoint to receive and store those requests.

The first thing I did was to create something which does exactly that. It's a relatively small application written in Go with just two endpoints (one for receiving a POST, the other for exposing a health check that can be monitored) and supported by a MongoDB database.

The application is called event-store. Right now we're using it in GOV.UK's preview environment for CSP reports, but we hope to improve it so that we can collect other kinds of events too, like JavaScript errors and frontend performance metrics such as the Navigation Timing API.

Adding the CSP header to the homepage

After setting up the reporting application, I added a CSP header to the GOV.UK homepage.

As with the reporting application, we're setting the header in our preview environment to see how it works. It's also not enforcing the policy just yet, but the -Report-Only suffix allows us to receive reports and make sure it's working as we expect.

The header whitelists a few sources, including the hostname we use for serving our assets. It also ensures that JavaScript from Google Analytics can be loaded, as well as the single pixel image request that Google Analytics makes to receive data.

Limitations of the current state of CSP

You might have noticed that in the CSP header I've enabled 'unsafe-inline' for JavaScript and CSS. The specification includes a severe warning about this:

authors should not include 'unsafe-inline' in their CSP policies if they wish to protect themselves against [cross-site scripting].

We haven't included this directive by accident, but the current state of CSP means that we have to allow all inline JavaScript at the moment.

GOV.UK includes inline JavaScript like this block to add a js-enabled class to the body. We'd prefer this script block to be inline for performance reasons.

The CSP 1.0 specification is a Candidate Recommendation at the W3C, which means it's in use and is fairly mature. This is the version that's implemented in current browsers (Chrome 39 and Firefox 36). Content Security Policy Level 2 is a W3C Last Call Working Draft, so it's mostly complete but hasn't been released in browsers yet.

CSP 2 includes the ability to whitelist inline JavaScript by including a hash of the script in the header. Using the js-enabled inline JavaScript as an example, you take the contents of the script tag:

document.body.className = ((document.body.className) ? document.body.className + ' js-enabled' : 'js-enabled');

Then calculate its SHA-256 digest and then Base64 encode that value:

$ echo -n "document.body.className = ((document.body.className) ? document.body.className + ' js-enabled' : 'js-enabled');" | openssl dgst -sha256 -binary | openssl enc -base64

Including that string in the CSP header will allow the inline script to execute without the 'unsafe-inline' directive:

Content-Security-Policy: script-src 'sha256-+6WnXIl4mbFTCARd8N3COQmT3bJJmo32N8q8ZSQAIcU='

Testing this header reveals that it works as expected in Chrome 40. The current beta release of Firefox (version 36) executes the script, but logs in the console to say that the script has been blocked (I've opened a bug about that).

This part of the CSP 2 specification works well, but because it's not widely available in browsers yet we're unable to use CSP to prevent inline JavaScript from running. Allowing inline JavaScript removes some of the protection that CSP provides, but the site is still more secure than if it didn't have a Content-Security-Policy header at all.

The future of Content Security Policy on GOV.UK

We'll be keeping an eye on changes to the CSP specification and watching as newer versions are gradually adopted by browsers. This work is just a starting point for adopting Content Security Policy across GOV.UK, and like everything else we'll keep on improving it to make sure our users are as safe as possible.