Content Security Policy Pt I: Javascript
May 23, 2018 · 1399 words · 7 minute read
Content Security Policy (CSP) is a mechanism you can use to prevent Cross-site Scripting (XSS) attacks on your website. An XSS attack is an attack where a hacker has found a way to get some malicious code into your website source code that runs for all visitors. This may be for example a piece of javascript that sends all cookies to the attacker so they can impersonate visitors, or a bit of javascript that starts mining cryptocurrency on behalf of the attacker or any other kind of nefarious script.
The are multiple ways they could get their code running in your website. For example if you have a query parameter in your URL that you display directly on the website, or a comment form that allows for HTML, etc. There are ways to prevent XSS from these sources, like sanitising or escaping the value so it becomes a harmless string and will not be interpreted as HTML, but since we are human and aren’t flawless we might forget sometime, allowing an XSS attack to happen.
As a broader meassure against XSS we can use CSP to instruct the browser what external sources may and may not be loaded. So that even if an attacker manages to get a bit of javascript on your website it will be useless because browsers will refuse to execute it.
An example: Google Analytics
A Content Security Policy is sent to the browser though a HTTP response header (alongside status code, Content-Type, Content-Length, etc). HTTP meta tags are also allowed, but I would not recommend that as it’s part of the page content, which is easier to manipulate in an XSS attack than a response header.
Content-Security-Policy can constraint a lot of different resources, like Javascript, CSS, images, fonts, etc. For this blog post though we will focus only on Javascript.
Now let’s start off with the most basic CSP for Javascript, which is to only allow scripts from the current domain, no other domain.
Content-Security-Policy: script-src 'self';
This means that any script on the page that is not hosted by the website itself will be ignored by the browser (and an error will be shown in the developer console about it). Also, with this CSP inline scripts (<script>
tags that directly contain code, and not src
attribute) will not be executed, and the method(s) that eval (eval
and the Function
class, and some others) will be disabled in Javascript. This is because you would have to explicitly allow them in your CSP.
So for example if you use the CSP above and you have Google Analytics on your website it will not work because the Google Analytics Javascript will not be loaded from their server. Also, the inline javascript snippet they suggest you use will not be executed.
First, to allow loading scripts from the Google Analytics domain, you would need to change the CSP to:
Content-Security-Policy: script-src https://www.google-analytics.com/ 'self';
(order does not matter here, script-src 'self' https://www.google-analytics.com/;
would also work)
So now all scripts from our own domain, as well as all scripts from google-analytics.com are allowed. However, Google Analytics will still not work because the Javascript they suggest you use by default uses inline javascript, and as stated above that is not allowed to be executed when not explicitly allowed in the CSP. To solve this we have no less than four options, which will be explained below.
Option 1: Allow inline javascript
Of all four options this is one is the easiest, but also by far the worst, and I vehemently recommend against using it. It’s only included for completeness sake.
You can add unsafe-inline
to your CSP, which tells the browser it is allowed to execute any inline Javascript. The header would then become:
Content-Security-Policy: script-src https://www.google-analytics.com/ 'self' 'unsafe-inline';
This defeats the entire purpose of having a CSP in the first place, since anything can be injected in an XSS attack again.
Option 2: Allow inline javascript with a nonce
Here you need generate a random string of about 40 characters every request and include that in the CSP header, like so:
Content-Security-Policy: script-src https://www.google-analytics.com/ 'self' 'nonce-fa5b78fa08487ed2bb9ddc8ca3c2aaf58eae9744';
And then with every inline script you need to include that nonce as well:
<script nonce="fa5b78fa08487ed2bb9ddc8ca3c2aaf58eae9744">alert('Hello world');</script>
This prevents XSS attacks because the nonce is different for each request, and a hacker has no way of guessing which value will come next, so they cannot inject a script with the correct nonce, so anything they can inject will not be executed by the browser.
Option 3: Allow inline javascript with a hash of the contents
This goes a bit further than the nonce because it adds a checksum to the script to be executed in the form of a SHA hash. For example, if we want to include the following on our website:
<script>alert('Hello world');</script>
Then what we need to do first is calculate the base64 version of the SHA sum of alert('hello world')
1, which is DUTqIDSUj1HagrQbSjhJtiykfXxVQ74BanobipgodCo=
.
Now we can include that in our CSP header:
Content-Security-Policy: script-src https://www.google-analytics.com/ 'self' 'sha256-DUTqIDSUj1HagrQbSjhJtiykfXxVQ74BanobipgodCo=';
This example uses SHA-256, but you can use SHA-384 or SHA-512 as well.
The script will now be executed by the browser without errors and without any need to change the <script>
tag in any way.
The advantage here is that we can be sure that no one can temper with the inline scripts because if even one character is changes the SHA sum is no longer valid and the script will not be executed anymore.
The drawback is that calculating SHA sums can be quite CPU intensive, on the server as well as the client. So if you have a lot of big scripts this might not be advisable.
Option 4: Don’t use inline scripts at all
Instead of working around the fact that you can’t use inline scripts anymore, just don’t use inline scripts anymore. Move your script to a separate .js
file and include that in your HTML.
This also keeps your HTML more clean and seperates concerns. Having a lot of javascript used to be advised against due to HTTP handshakes overhead, but now that more and more web servers are running HTTP/2 this is becoming less and less of an issue. And of course you can still combine and minify scripts if you want to.
More on external scripts
So now we’ve looked at inline scripts, how about external scripts? We’ve seen we can include a domain in our CSP header to be allowed, but that is still quite broad, since that means any script from that domain may be loaded, and while you may trust a single script on that domain, you don’t necessarily trust all scripts on that domain.
Well, there is good news and bad news. The good news is that the nonce and SHA methods described above also work for external scripts as of CSP Level 3, the bad news is that not all browser support this yet, most notably Microsoft Edge (at the time of writing, at version 16). So basically you can use nonce and SHA for external scripts, but you still need to add the domain as well as fallback for browsers that don’t support CSP Level 3 yet. There is also Subresource Integrity (SRI), but that doesn’t work in Edge either.
So what we would want is a way to get CSP Level 3 compliancy in browsers that support it, with proper fallbacks for browsers that don’t support it. We can achieve this by using the ‘strict-dynamic’ keyword in our CSP:
Content-Security-Policy: script-src https://www.google-analytics.com/ 'self' 'nonce-fa5b78fa08487ed2bb9ddc8ca3c2aaf58eae9744' 'strict-dynamic';
This tells the browser that any script with a nonce or SHA hash on your site may require additional scripts from different domains and alls those scripts are allowed to be executed as well. The nice thing about ‘strict-dynamic’ is that when you do include it, CSP Level 3 compliant browsers will no longer use any whitelisted domains, ‘self’ and ‘unsafe-inline’, but older browsers will still use them and ignore ‘safe-unline’ (because they don’t know what it is). This basically reduces the CSP to just allowing scripts with a nonce, SHA, and scripts that were included by those scripts for CSP Level 3 compliant browsers, and the to the whitelist of domains for CSP Level 2.
1 We can obtain this by running echo -n "alert('Hello world');" | openssl dgst -SHA256 -binary | openssl enc -base64
in our *nix terminal