Content Security Policy

In this post I'm going to take a look at Content Security Policy and how it can help secure my site.

What is Content Security Policy?

Content Security Policy (CSP) can help protect my site and visitors by telling browsers to place restrictions on what content can be included on a page.

It can prevent code that isn't trusted from being included on the page or run by browsers. It can restrict an iframe to only run content from certain domains.

It can also prevent all inline scripts and styles from running, or even only allow an inline script/style if its hash matches that in the header.

How Can It Help?

There are a lot of ways of injecting code on a Web page.

Cross-site Scripting (XSS) attacks get the browser to load content from external sites, typically with the aim of capturing (stealing) data such as session cookies or form input.

If twitter were compromised at present, the twitter widget could be modified to include bad stuff.

Compromise Separation

One good thing about CSP is that even if someone compromises the files that make up the Web site, any loading of external content can be prevented by the Web server and/or proxy instances by removing any Content-Security-Protocol headers and adding your standardised Content-Security-Protocol header.

For example, if someone were to modifiy the code on this page so that a script is loaded from an external site, and they were to add a Content-Security-Policy header via PHP, that external script wouldn't load if nginx and/or my varnish caches were to strip that header and add a standardised (for site/page) header.

In the case of Web.JohnCook.UK, they would need to compromise the nginx instance on my VPS. By compromising that they would be able to do anything with my site anyway, and would possibly have access to my TLS private key, so such a compromise would be large in any case.

But a compromise of the content of the site would not be a compromise of nginx.

My Content Security Policy

Inline Styles and Scripts

Creating hashes of the JS and CSS is too much hassle, having just tried it, so I am going to use unsafe-inline.

I am therefore going to start with a very simple CSP:

Content-Security-Policy: default-src https:; child-src 'none'; object-src 'none'; script-src https: 'unsafe-inline'; style-src https: 'unsafe-inline';

External Content

On my site I currently only use content from Twitter and Youtube. To include this content in my CSP I just need to change it to the following:

Content-Security-Policy: default-src https:; child-src https://www.youtube-nocookie.com https://platform.twitter.com; object-src 'none'; script-src https: 'unsafe-inline'; style-src https: 'unsafe-inline'; img-src https://web.johncook.uk https://syndication.twitter.com data:;

Fonts

My fonts come from two places: the site and Google. All 3 of my domains use web.johncook.uk as a "static" source for fonts (and other static content) so there is no need to use 'self'.

Content-Security-Policy: default-src https:; child-src https://www.youtube-nocookie.com https://platform.twitter.com; object-src 'none'; script-src https: 'unsafe-inline'; style-src https: 'unsafe-inline'; font-src https://web.johncook.uk https://fonts.gstatic.com;

Images

All images on the site come from web.johncook.uk with the exception of the twitter share button which comes from https://syndication.twitter.com. I also use some data: URIs for images.

Content-Security-Policy: default-src https:; child-src https://www.youtube-nocookie.com https://platform.twitter.com; object-src 'none'; script-src https: 'unsafe-inline'; style-src https: 'unsafe-inline'; font-src https://web.johncook.uk https://fonts.gstatic.com; img-src https://web.johncook.uk https://syndication.twitter.com data:;

XHR (XmlHttpRequest)

I use some JSON on my site. At present it is mainly for alerts and social network sharing.

Unlike images, the JSON is not from the same domain across multiple sites. Rather than hard-coding all variants of the 3 domains used by the site, I will use a connect-src policy of 'self'.

Content-Security-Policy: default-src https:; child-src https://www.youtube-nocookie.com https://platform.twitter.com; object-src 'none'; script-src https: 'unsafe-inline'; style-src https: 'unsafe-inline'; font-src https://web.johncook.uk https://fonts.gstatic.com; img-src https://web.johncook.uk https://syndication.twitter.com data:; connect-src 'self';

Scripts

Scripts and stylesheets are the two things where I need to tighten up things. At present all https: URIs are permitted, as well as 'unsafe-inline'. The first tightening shall be replacing https: with specific domains:

Content-Security-Policy: default-src https:; child-src https://www.youtube-nocookie.com https://platform.twitter.com; object-src 'none'; script-src https://web.johncook.uk https://ajax.googleapis.com https://platform.twitter.com 'unsafe-inline'; style-src https: 'unsafe-inline'; font-src https://web.johncook.uk https://fonts.gstatic.com; img-src https://web.johncook.uk https://syndication.twitter.com data:; connect-src 'self';

Stylesheets

I think I only use stylesheets inline and from web.johncook.uk.

Content-Security-Policy: default-src https:; child-src https://www.youtube-nocookie.com https://platform.twitter.com; object-src 'none'; script-src https://web.johncook.uk https://ajax.googleapis.com https://platform.twitter.com 'unsafe-inline'; style-src https://web.johncook.uk 'unsafe-inline'; font-src https://web.johncook.uk https://fonts.gstatic.com; img-src https://web.johncook.uk https://syndication.twitter.com data:; connect-src 'self';

Correction: I also use a stylesheet from https://fonts.googleapis.com to load the fonts.

Content-Security-Policy: default-src https:; child-src https://www.youtube-nocookie.com https://platform.twitter.com; object-src 'none'; script-src https://web.johncook.uk https://ajax.googleapis.com https://platform.twitter.com 'unsafe-inline'; style-src https://web.johncook.uk https://fonts.googleapis.com 'unsafe-inline'; font-src https://web.johncook.uk https://fonts.gstatic.com; img-src https://web.johncook.uk https://syndication.twitter.com data:; connect-src 'self';

Testing

To test my CSP, I am simply using Chromium (with Developer Tools visible) and adding the header in nginx for this page only:

        location @varnish {
                if ($request_uri ~ ^/blogs/website/content-security-policy$) {
                        add_header Content-Security-Policy "default-src 'none'; child-src https://www.youtube-nocookie.com https://platform.twitter.com; object-src 'none'; script-src https: 'unsafe-inline'; style-src https: 'unsafe-inline'; font-src https://web.johncook.uk https://fonts.gstatic.com; img-src https://web.johncook.uk https://syndication.twitter.com data:; connect-src 'self';";
                }
                proxy_pass      http://johncook.varnish;
                include /etc/nginx/includes/proxy.static;
                sendfile on;
                tcp_nopush off;
                tcp_nodelay on;
                keepalive_requests 500;
                proxy_http_version 1.1;
                proxy_set_header Connection "";
        }

Sandboxing Iframes

Iframes can be sandboxed. I believe I currently only use iframes with YouTube content, and I generally transform the following YouTube supplied embed code:

<iframe width="420" height="315" src="https://www.youtube-nocookie.com/embed/rSFXtCUc1HQ" frameborder="0" allowfullscreen></iframe>

To the following:

<div class="flex-video">
<iframe src="https://www.youtube-nocookie.com/embed/rSFXtCUc1HQ" frameborder="0" allowfullscreen></iframe>
</div>

With the sandbox attribute (most restrictive) the following code does not work with YouTube embedding:

<div class="flex-video">
<iframe src="https://www.youtube-nocookie.com/embed/rSFXtCUc1HQ" frameborder="0" allowfullscreen sandbox></iframe>
</div>

To be more permissive, the following attribute values (space-separated) are available for the sandbox attribute: allow-forms, allow-pointer-lock, allow-popups, allow-same-origin, allow-scripts, allow-top-navigation.

Embedded YouTube videos don't work without scripting being enabled, and CORS on YouTube requires allow-same-origin. To allow clicking the YouTube icon in the video to launch the video in a new tab, allow-popups is also required.

The following video was embedded using the code that follows it, with all the YouTube buttons in it working.

I spent a long time trying to get it to work with the code before the video, but it looks like something somewhere prevents the embedding from working if the embed code has already appeared on the page (even if only in a code block).

Thumbnail of YouTube video: [Linux.conf.au 2013] - Defeating Cross-Site Scripting attacks with Content Security Policy

Video: [Linux.conf.au 2013] - Defeating Cross-Site Scripting attacks with Content Security Policy

Play Video Embedded Watch Video on YouTube Google Privacy Policy Google Cookies
<div class="flex-video">
<iframe src="https://www.youtube-nocookie.com/embed/rSFXtCUc1HQ" frameborder="0" allowfullscreen sandbox="allow-scripts allow-same-origin allow-popups"></iframe>
</div>

Admittedly allowing scripts and popups isn't the most secure way of doing things, but it is more secure than not sandboxing.

Don't Frame Me

Some unscrupulous people load your content in frames on their site. Googlebot will then come along, index their domain (which includes frames on their pages with a source of a page on your site), and sooner or later you might be faced with their site outranking your own.

The old way of handling clickjacking is to use a X-Frame-Options header. The new way is to use CSP frame-ancestors.

Since my site doesn't use framing (including iframes) for any internal content, it will be perfectly fine to use a policy of 'none'. If, in future, I embed internal static content (such as music) I might have to adjust this for specific file extensions.

Content-Security-Policy: default-src 'none'; child-src https://www.youtube-nocookie.com https://platform.twitter.com; object-src 'none'; script-src https://web.johncook.uk https://ajax.googleapis.com https://platform.twitter.com 'unsafe-inline'; style-src https://web.johncook.uk https://fonts.googleapis.com 'unsafe-inline'; font-src https://web.johncook.uk https://fonts.gstatic.com; img-src https://web.johncook.uk https://syndication.twitter.com data:; connect-src 'self'; frame-ancestors 'none';

Other Directives

default-src includes connect-src, font-src, img-src, media-src, object-src, script-src, style-src, and child-src. It doesn't include frame-src (replaced with frame-ancestors in CSP2), report-uri, sandbox, form-action, frame-ancestors, plugin-types, or base-uri.

With frame-ancestors added, the only things not included are report-uri, sandbox, form-action, plugin-types, and base-uri.

I don't use the <base> element on my site, so base-uri can be set to 'none'. I also at present don't use any plugins (as evidenced by restriction of object-src to 'none') nor do I use forms. I can tighten things up a bit more by adding directives for form-action and base-uri. If I eventually use plugins, I will need to amend object-src and add plugin-src.

Content-Security-Policy: default-src 'none'; child-src https://www.youtube-nocookie.com https://platform.twitter.com; object-src 'none'; script-src https://web.johncook.uk https://ajax.googleapis.com https://platform.twitter.com 'unsafe-inline'; style-src https://web.johncook.uk https://fonts.googleapis.com 'unsafe-inline'; font-src https://web.johncook.uk https://fonts.gstatic.com; img-src https://web.johncook.uk https://syndication.twitter.com data:; connect-src 'self'; frame-ancestors 'none'; form-action 'none'; base-uri 'none';

Final Testing

Since I cannot remove 'unsafe-inline' from script-src or style-src until I change how I do things (potentially not possible due to loading fonts using JavaScript and using Google Fonts) this Content-Security-Policy is adequate. With it working on one page it is now time to apply it site-wide by removing the if block in nginx. I'll also add it to the @dynamic and @static location blocks in nginx.

The Network Status page includes a twitter widget for my @WatfordJC_WAN twitter account. I need to add https://cdn.syndication.twimg.com to script-src.

In order to get the Network Status page to work, I need to make a few more modifications:

Content-Security-Policy: default-src 'none'; child-src https://www.youtube-nocookie.com https://platform.twitter.com; object-src 'none'; script-src https://web.johncook.uk https://ajax.googleapis.com https://platform.twitter.com https://cdn.syndication.twimg.com https://syndication.twitter.com 'unsafe-inline'; style-src https://web.johncook.uk https://fonts.googleapis.com https://platform.twitter.com 'unsafe-inline'; font-src https://web.johncook.uk https://fonts.gstatic.com; img-src https://web.johncook.uk https://syndication.twitter.com https://platform.twitter.com https://pbs.twimg.com data:; connect-src 'self'; frame-ancestors 'none'; form-action 'none'; base-uri 'none';