Content Security Policies
A couple of days ago I was at the Dutch PHP Conference where I went to a talk about Content Security Policies by Matt Brunt. Although we follow the OWASP top ten list, I never dived into them that much. We have written a bit about them before, but I want to go a little further since they are really handy when trying to prevent XSS attacks.
Even though most websites use battle-tested frameworks and libraries which automatically escape XSS, it is still number 7 in the OWASP top ten list. Also big companies like Facebook, Twitter or Yahoo still suffer from XSS from time to time. So we may conclude that it is hard to create a bulletproof defense against it. But if you have a good content security policy implemented you will make it extra hard (if not impossible) for attackers to inject harmful code into your websites.
Meet content security policies
A content security policy is meant as extra security layer in your defense against XSS attacks. With it you have control over what may, and more importantly, what may not be loaded on a webpage. You can tell the browser things like to only trust scripts from a certain domain and block all the others coming from different domains. Or you can tell the browser to not trust (and thus execute) inline scripts or styles except for the ones you specifically allowed.
It is by no means a silver bullet. It isn’t supported in all browsers and you still have to be careful with user input. But it won’t harm your site if it’s not supported (older browsers will simply ignore the policy) and adding it can reduce the risk of getting infected by a lot.
How to use
The implementation of a CSP is actually very easy, you essentially only have to add a single header to your website’s responses. But don’t let this fool you because a wrong implementation can lead to headaches and broken websites. We’ll talk later about reporting which we can use in order to make sure a wrong implementation doesn’t lead to a broken website.
A CSP header consists of one or more directives (img-src, script-src, form-action …). Each directive contains one or more sources (which can be urls or words like “self” to point to the current domain).
So let’s look at a simple CSP header:
Content-Security-Policy: default-src 'self';
This will only allow resources from the same domain to be loaded. So things like images, scripts or even swf objects are only loaded when they live on the same domain as the current webpage. The “default-src” directive acts as a fallback if no specific policy is set for a resource, the “self” points to the current domain. It also blocks all inline scripts and styles because that is the default behaviour for CSPs. If we want to use inline scripts on our website, we can add a “script-src” directive with the “unsafe-inline” source:
Content-Security-Policy: default-src 'self' script-src 'self' 'unsafe-inline';
Notice that the “script-src” also contains the ‘self’ source. Directives can’t extend each other, so even though “default-src” is used as fallback, the “script-src” will ignore it’s policy.
CSPs support wildcards, so if we for example only want to allow forms to be posted to subdomains of ibuildings.nl, we can implement it like so:
Content-Security-Policy: form-action *.ibuildings.nl
By the way, if you somehow can’t add headers to your responses, you can use a <meta> tag instead:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'"/>
But do note that the meta tag does not support all directives.
Have a look at the MDN documentation for more information about the directives you can use and their possible values.
Hashes and nonces
CSPs also allows you to specifically allow a single script or style on a webpage by using hashes and/or nonces. By using hashes you need to calculate the hash value of a script (or style). On linux that can be done with:
$ echo -n "alert('Something...');" | openssl dgst -sha256 -binary | base64
To use the result of this in a CSP, you need to prefix it with the name of hash-algorithm you used. So in the case of the example above we need to prefix it with ‘sha256-’:
Content-Security-Policy script-src 'sha256-b8VXdG+ZKJSb7A9AdvEsunm/cEn3YXSExP5VkQpnh/c='
But you need to recalculate the hash every time the script changes, which can get tiresome very quickly. So you can use a nonce (number used once) instead. To do this, you need to generate a random value every request, base64 encode it and add it to the CSP header prefixed with ‘nonce-’
Content-Security-Policy script-src 'nonce-c29tZXRoaW5nCg=='
And a “nonce” attribute has to be added to the script element:
Although nonce stands for “number used once”, it can be used on multiple elements. But it should be regenerated every request (so don’t use hardcoded values!).
It’s best to only use nonces for inline scripts/styles. You can use them for external resources, but Edge has a bug so it’s better to allow certain domains instead until they fix it.
But what if we use something like google analytics on our website, and forgot to allow it in our CSP? It will be blocked from loading, but we can only know that by loading the webpage ourselves and checking the browser’s console. For this the CSPs have the “report-uri” directive (or the newer “report-to”). If anything get’s blocked by our CSP it will send a JSON report to the given uri:
Content-Security-Policy: default-src 'self'; report-uri https://example.org/csp-reports
While you can write your own endpoint which receives the report and saves it somewhere, you can use existing tools like report-uri.com which give you a nice overview of all received reports, graphs etc.
But our website can still break because something is getting blocked by the CSP, we now only have a report about it. Luckily we can use the “report only” mode which will only validate the policies, but won’t enforce it. In other words; if we use the report-only mode we still get all the reports, but nothing gets blocked. To enable this mode, just add “Report-Only” to the header name:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri https://example.org/csp-reports
It’s highly recommended that you run a CSP in report only mode first, before actually enforcing it. You can use the “Content-Security-Policy” header together with the “Content-Security-Policy-Report-Only” header. So you can enforce a policy while testing a newer policy.
We at Ibuildings think it’s important to include security in all of our projects. Implementing a CSP is one of those measures and is very easy to do. But it’s also very easy to break your website with it, so implement with care! If you run into issues or don’t know how to do some things you can always contact us. We are glad to help you!