Tune The Web

Content Security Policy (CSP)

Introduction

An web browser is an enormously powerful and versatile piece of software, which is able to load resources from all over the internet to make up a website. This makes it a security nightmare! Extra code can be included in your website in a variety of ways, especially in the web 2.0 world we live in, where web page content is created by users.

Cross site scripting (XSS) is almost always near the top of any security list of web vulnerabilities (it's number three in the latest OWASP Top 10). This basically involves putting unauthorised javascript onto a web page. This can be done in a variety of ways, say for example you have a search function on your website, and it printed the search term at the top of the page. So going to www.tunetheweb.com/searchresults/?q=RandomSearchTerm, would print RandomSearchTerm at the top of the page. Similarly doing a search for <script>alert(1);</script> would print <script>alert(1);</script> at the top of the page, and your browser would recognise this javascript and run it and pop up an alert. This is just one way of injecting javascript (and is protected on this website so don't even waste your time doing that search btw!).

Other ways include maybe putting a comment on a site that has a comments section (like this one). That way, any other visitor to the website, will load that comment, and execute any javascript (again this shouldn't happen on this website as our commenting system from Disqus checks for these attacks). Other ways are compromising any other content you load on your site (e.g. if you load a bit of code from another website onto your website for a fancy animation for example).

While javascript is probably the most dangerous item to have injected into your website, other content also can cause problems. CSS could be used to hide content, for example, replacing a form where you enter a credit card with another form that sends data to an attacker. 1 pixel images could be used to track users over the Internet. Frames could be used to load page views, wasting user bandwidth and creating fake hits on another site. The list goes on and on.

Content Security Policy (CSP) attempts to address these issues by allowing a website to say exactly what resources to load onto a website. Website owners should, in theory, know exactly what resources are needed by their website, so why not tell the web browser not to load anything else?

How to set it up

A CSP policy is set up by sending a HTTP Header in a certain format. An example is shown below:

Header always set Content-Security-Policy: "default-src 'self' https:; script-src 'self'; style-src 'self';"

This states that, unless otherwise specified, resources can be loaded from this website and any other website running HTTPS. Javascript can only be load from this website, and so can CSS styles. So this stops any inline scripts, and also any external scripts being loaded. Such a policy would basically address XSS for your site.

There are a variety of different settings you can send in this header and Scott Helme has an great CSP Cheat Sheet of the settings you can use, which allows you to build up a complicated policy.

Other major options include the ability to add a reporting mechanism for any policy violations to send JSON reports to a specified URL:

Header always set Content-Security-Policy: "default-src 'self' https:; \ script-src 'self'; style-src 'self'; \ report-uri https://www.tunetheweb.com/api/report/csp-report.json;"

This allows you to monitor if parts of your website are being blocked, by either an incorrect policy, or because of an incorrect browser implementation.

There is also the option of running in report only mode, which send back reports, but will not block resources from being loaded:

Header always set Content-Security-Policy-Report-Only: "default-src 'self' https:; \ script-src 'self'; style-src 'self'; \ report-uri https://www.tunetheweb.com/api/report/csp-report.json;"

This is highly recommended before you implement CSP, to test out your policy is correct and will not inadvertently block resources your website needs. This is especially if you run a large website, with many pages, and you can't be 100% sure that your policy covers every resource on every page.

Either reporting method (whether block and report with Content-Security-Policy or just report with Content-Security-Policy-Report-Only), requires a service to be set up to listen to the reports. Check out report-uri for this, to save you setting up your own one, though setting up your own is as simple as creating something that listens for JSON POST requests and then actions them. A simple example NodeJS service to list to CSP and HPKP and just log these to a log file is shown below:

'use strict' var express = require('express'), bodyParser = require('body-parser'), debug = require('debug')('ReportService'), errorhandler = require('errorhandler'), morgan = require('morgan'), compression = require('compression'); var app = express(); app.disable('x-powered-by'); app.use(errorhandler({ dumpExceptions: true, showStack: true })); //app.use(express.json()); app.use(morgan( 'dev', { immediate: true })); app.use(compression()); app.use(function(req, res, next) { //Some browsers (Chrome at least) doesn't set the content type for HPKP //Reports which causing the body parser to ignore the content, so add the content type. if(typeof(req.headers['content-type']) === 'undefined'){ req.headers['content-type'] = "application/json; charset=UTF-8"; } next(); }) // parse various different custom JSON types as JSON app.use(bodyParser.json({ type: 'application/json' })); app.use(bodyParser.json({ type: 'application/csp-report' })); app.get('/test/', function (req, res) { res.send('I am running!\n'); }); app.get('/report/csp-report.json', function (req, res) { res.status(403); res.send('Should only be a POST request!'); }); app.get('/report/hpkp-report.json', function (req, res) { res.status(403); res.send('Should only be a POST request!'); }); app.post('/report/csp-report.json', function (req, res) { debug('debugReport for CSP:' + '{"Headers":' + JSON.stringify(req.headers) + ',"Body":' + JSON.stringify(req.body) + "}"); res.send('Thanks for reporting'); }); app.post('/report/hpkp-report.json', function (req, res) { debug('debugReport for HPKP:' + '{"Headers":' + JSON.stringify(req.headers) + ',"Body":' + JSON.stringify(req.body) + "}"); res.send('Thanks for reporting'); }); app.listen(3002); debug(JSON.stringify(3002) + ":port starting");

However I should offer a warning about setting up a report-uri option: they are very, very noisy. Lots of browser extensions inject content into the page, which of course CSP blocks, but then reports back to your report-uri URL. Now reports are only really useful if there is something for you to action based off of (e.g. to act as a warning of an incorrect CSP policy), but for browser extension reports there's not much you can do except ignore them (which is easier said than done and adds risk of a real alert being incorrectly identified as a false positive), or allow them to reduce the noise (which leads to a more lax policy than you really want to have). Neither is great.

Also see note below about Chrome and Safari having a bug when both Content-Security-Policy and Content-Security-Policy-Report-Only headers are used at the same time.

The above policies will not allow inline scripts, which you might want in some very specific case. This website uses Google Analytics for example, which is recommended to have as an inline script on the page, and then loads the Javascript from the Google servers. The above policies will prevent both the inline script, and the Google script being loaded. You could use the unsafe-inline policy option for the inline script:

Header always set Content-Security-Policy: "default-src 'self' https:; \ script-src 'self' 'unsafe-inline' https://www.google-analytics.com;"

However that opens up your website to all <script> tags anywhere on the page. CSP 2 adds the options for hashes, where you can calculate a hash based on the text of the script, and then allow that hash rather than all inline scripts using unsafe-inline. The new policy would look like this:

Header always set Content-Security-Policy: "default-src 'self' https:; script-src 'self' \ 'sha256-iLa0hRPsoDjvxFYYu-uNM5UI6luIzGqIS_ir0v4_XH0=' https://www.google-analytics.com;"

The easiest way to find the hash needed btw, is to load the page in Google Chrome with the policy without the has, and look at the Console in Developer Tools which helpfully will tell you the hash you need to add to your policy:

Another option, similar to using hashes, is to use a randomly generated nonce value. This is slightly trickier to set up, depending on the technology on your website, but would involve generating a random number, and setting that random number as a parameter to the script tag (<script nonce-RANDOMNUMBER>), and then adding that random number to the CSP header returned when that page is requested.

Note that when you specify a hash or a nonce, most browsers, will then automatically remove any unsafe-eval option from your policy under the assumption that you don't need both. This can cause problems if you want to start adding hashes and you've a few to get to on your site - it becomes an all or nothing option. Firefox has an even worse implementation, whereby including hashes turns off unsafe-eval, but hashes are not supported meaning all inline scripts are suddenly turned off! This bug (tracked here and which looks to be fixed now), basically stopped you being able to use hashes at all, even for browsers that do support them properly like Chrome. Firefox also prints an error saying the same thing happens for nonces, but then it goes ahead and loads the resource anyway!

One final thing to note is that the policy is only really needed on the main page, and not on all the other resources (CSS files, Javascript files, images, fonts...etc). This is especially important as the policy can get quite large and so sending that back on every resource ends up being a bit of a waste. This can easily be set up, in Apache with config like the following:

<FilesMatch "\.(htm|html|php)$"> <IfModule mod_headers.c> Header always set Content-Security-Policy: "default-src 'self' https:; \ script-src 'self'; style-src 'self';" </IfModule> </FilesMatch>

This assumes you are only returning .htm, .html or .php files, but this can be extended to as many extensions as you need. I haven't found a way to send this header based on the returned MIME type (e.g. text/html).

Support

Web server support is not necessary, as it just has to send back a header you set and not actually know anything about that. Web browser support of CSP is mixed. Although CSP 1.0 is reasonably supported by modern browsers only Chrome and Opera support most of CSP 2, which give the more secure features which such as hash-source, nonce source, which allow you to remove the use of unsafe-eval.

The issue discussed above, with Firefox not supporting hashes, but then automatically removing the unsafe-inline policy if you specify any hashes, is a terrible implementation and effectively removes the ability to use hashes in any browser (unless you want to get into browser sniffing).

Chrome and Safari also seem to have issues with implementing both "Content-Security-Policy" and "Content-Security-Policy-Report-Only" policies at the same time, if they are different, which leads to weird errors like this, where it asks for the hash that is clearly already in the policy!:

Removing either header fixes the issue.

CSP 2.0 is pretty bleeding edge, and the bugs will probably be fixed soon, but doesn't exactly make it easy to use or inspire confidence when mainstream and modern browsers like Firefox and Chrome have these issues. Who knows how smaller less mainstream browsers will act.

Evven with that, support depends, not just on your browser, but also on what third part content you have loaded. This website used to load two Twitter items (a "Tweet this page" icon, and a "Follow me" icon) and they needed unsafe-inline scripts to work (shame on Twitter and I hope you fix this!). I actually ended up replacing the official Twitter ones with my own for a few reasons including this. I also use the Disqus commenting tool which requires unsafe-inline CSS styling, and finally Google Custom Search which requires unsafe-eval and unsafe-inline on the script src (shame on Google!). This removes most of the policies you would ideally want to set up, and this is a very simple website with basically static content other than those plug-ins. The more plug-ins you have, the more lax you are likely to have to make your policy. This is especially relevant if you are loading content that is liable to change (for example advertisements which might display different content frequently).

The downsides

The major downside, is that it's difficult to set up correctly unless you know all the resources used across your website. There are tools to try to help you create your CSP policy, but even they take effort. And as soon as you are loading third party content (e.g. advertisements) then a policy mike be almost impossible to implement without risking blocking some of those third party resources.

Even if you do know the resources used by your site, you probably want to clean it up to put in a safe policy, so there may be a big effort required in that. If you have no third party items on your site, then removing your own inline-scripts, to prevent the need of unsafe-inline is fairly easy in principal (just move them to .js files and load them). However this could take a lot of effort and also may produce a small performance hit (as this will cause additional network requests to load .js files that may previously have loaded inline). It is good to do this clean up, but until you do across the entire site, you will need to include the unsafe-inline option, which is one of the main ways XSS is implemented. If you do have 3rd party content (as is likely!) then this takes even more effort or may not be possible - as mentioned above, this site requires unsafe-inline on scripts (for Twitter plug-in) and styles (for Disqus) and even unsafe-eval (for Google Custom Search), which kind of defeats the point! Eventually, once support is there for CSP 2 in all browsers, those might be able to be replaced by hashes, or for 3rd parties to write plug-ins with CSP in mind.

The other issues is that even if you do get your policy set up correctly, and everything works, there is no guarantee that a third party item will not change something and break your site in future. Whether this causes your site real issues depends on what those resources are. It could be as simple as a bit of animation not work, or suddenly you start blocking all your advertisements because Google changes the way AdWords loads, or set up a new domain, which could have a major impact to the revenue of your site. Then again the alternative point of view is that if you are loading third party content then you have effectively given up the control of what is loaded in you site, and so CSP is not really for you but that seems a little defeatist.

The limited support of CSP 2 at the time of writing as discussed above, effectively means most sites will require unsafe-inline - which massively limits the usefulness of CSP for now. Additionally the current bugs can easily break a site. For example, if you have a working policy, and want to test out a new more secure one, then adding a reporting policy, might seem like a safe and sensible thing to do but will break your current site on Chrome at least.

Policies may also get quite large. The correct policy for this site looks like this:

Header always set Content-Security-Policy-Report-Only: "default-src 'none'; \ base-uri 'self'; font-src 'self' https://a.disquscdn.com; \ script-src 'self' https://www.google-analytics.com https://cse.google.com \ https://www.google.com https://www.googleapis.com https://tunetheweb.disqus.com \ https://a.disquscdn.com https://translate.googleapis.com https://translate.google.com \ https://cdn.ampproject.org https://www.gstatic.com; style-src 'unsafe-inline' 'self' https:; frame-ancestors 'none'; form-action 'none'; \ img-src 'self' https: data:; connect-src 'self' https://www.google-analytics.com;\ child-src 'self' https://disqus.com; frame-src 'self' https://disqus.com; media-src 'none'; \ object-src 'none'; report-uri https://www.tunetheweb.com/api/report/csp-report.json;"

This is a bit of a mouthful to say the least, and adds 750 characters to every request to my site. Now I do suggest you limit just to page requests as detailed above but that's still quite a lot of extra load. And this is a pretty simple site so that policy could easily grow and grow! This could end up with people choose the lowest common denominator and going with a very lax policy like, say, the following instead:

Header always set Content-Security-Policy: "default-src 'self' https: 'unsafe-inline' 'unsafe-eval';\ report-uri https://www.tunetheweb.com/api/report/csp-report.json;"

There is almost no benefit in this policy, except perhaps to limit resources to https: resources only (which most browsers will no anyway).

Finally CSP may break a lot of browser plug-ins and extensions which intentionally inject Javascript into your webpage for them to work as mentioned above. Any browser extensions which manipulates the DOM will trigger a CSP check and report. This leads to a lot of false reports (negating the use of the report-uri part, and also user frustration that their favourite extension no longer works on your site. At the moment I seem to get CSP violation reports for a third to half of my page views! None of these seem to be causing problems with my page, but I can't honestly tell you that I check these reports religiously when there are so many of them and a real false alarm could easily be breaking my site without me knowing.

I've a longer blog on some of the dangers of CSP and other security headers.

Summary

CSP is a very interesting security tool, which goes a long way to resolving Cross Site Scripting attacks (XSS) and puts some level of control of what you allow in your website back to website owners. However it is not without it's risks. Even scripts and tools from the largest and most net-savy companies like Google and Twitter currently would require a very lax CSP policy which many would consider defeats a lot of the point of having any CSP policy. This will improve over time, and browser support is increasing every day. However, for now I would suggest a very lax policy, and you should definitely run in Report-Only mode initially. A very lax policy I started with, before I tightened it up with a lot of effort, is given below and would make a good general policy as an initial step into CSP (changing the report-uri to your own reporting uri, obviously):

Header always set Content-Security-Policy-Report-Only: "default-src 'self' https: 'unsafe-inline' 'unsafe-eval'; \ img-src 'self' https: data:; \ form-action 'self'; \ frame-ancestors 'none'; \ report-uri https://your-report-uri;"

This should allow most scripts still to work, and basically only allow https assets, forms to be posted to yourself, no framing but reporting.

A more strict CSP policy should be aimed for, but may be difficult to achieve. However really secure sites, like internet banking sites, that do not (or at least should not!) host any third part content, should be easier to lock down and ultimately it's those sites that will benefit most from this. For the rest of us it's I think it's a matter of a watch this space for now.

This page was originally created on and last edited on .

How useful was this page?

Load more comments!