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.
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: 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
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)
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
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.
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.
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:
Then what we need to do first is calculate the base64 version of the SHA sum of
alert('hello world') 1, which is
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.
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