Content Security Policy is delivered via a HTTP response header, much like HSTS, and defines approved sources of content that the browser may load. It can be an effective countermeasure to Cross Site Scripting (XSS) attacks and is also widely supported and usually easily deployed.
Why do we need CSP?
nastyhackers.com and no way of knowing the content is malicious. This is where CSP comes in.
Whitelisting Approved Sources
A CSP header allows you to define approved sources for content on your site that the browser can load. By specifying only those sources that you wish the browser to load content from, you can protect your visitors from a whole range of issues. Here is a basic CSP response header.
Content-Security-Policy: script-src 'self'
script-src directive specifies the whitelist of sources that the browser may load scripts from. Using the
'self' keyword is easier than specifying my whole domain and makes the policy a little easier to read once it starts growing. Because the domain
nastyhackers.com isn't in the script whitelist, the browser will not load the script content from
What can we protect?
CSP comes with a wide range of directives that can be used to enforce policy across a whole load of content and circumstances. Here is a list of all of the directives that are available, along with a brief description, courtesy of OWASP.
default-src: Define loading policy for all resources type in case of a resource type dedicated directive is not defined (fallback),
script-src: Define which scripts the protected resource can execute,
object-src: Define from where the protected resource can load plugins,
style-src: Define which styles (CSS) the user applies to the protected resource,
img-src: Define from where the protected resource can load images,
media-src: Define from where the protected resource can load video and audio,
frame-src: Define from where the protected resource can embed frames,
font-src: Define from where the protected resource can load fonts,
connect-src: Define which URIs the protected resource can load using script interfaces,
form-action: Define which URIs can be used as the action of HTML form elements,
sandbox: Specifies an HTML sandbox policy that the user agent applies to the protected resource,
script-nonce: Define script execution by requiring the presence of the specified nonce on script elements,
plugin-types: Define the set of plugins that can be invoked by the protected resource by limiting the types of resources that can be embedded,
reflected-xss: Instructs a user agent to activate or deactivate any heuristics used to filter or block reflected cross-site scripting attacks, equivalent to the effects of the non-standard X-XSS-Protection header,
report-uri: Specifies a URI to which the user agent sends reports about policy violation
Creating A Policy
You can be as specific or as broad as you like when creating a CSP and fine tune it so that it meets your requirements exactly. Here are some example policies to show you what's possible.
Content-Security-Policy: default-src https: would allow any assets to be loaded over https from any origin.
Content-Security-Policy: default-src scotthelme.co.uk would allow any assets to be loaded from any origin on my domain using any scheme or port.
Content-Security-Policy: default-src https://scotthelme.co.uk:443 would only allow assets to be loaded from my domain, over https and on port 443.
Content-Security-Policy: default-src https://scotthelme.co.uk:* would only allow assets to be loaded from my domain over https on any port.
Wildcards can be used for the scheme, the port and the left most part of a hostname only.
*://*.scotthelme.co.uk:* would match any scheme on any subdomain of scotthelme.co.uk, but not scotthelme.co.uk itself, and on any port.
Further to this, you can also use the following keywords alongside the
'self' keyword mentioned above:
'none' blocks the use of this type of resource.
'self' matches the current origin (but not subdomains).
'unsafe-inline' allows the use of inline JS and CSS.
'unsafe-eval' allows the use of mechanisms like eval().
These can be applied in the following manner:
Content-Security-Policy: default-src 'none'; script-src https://scotthelme.co.uk would block the loading of any content apart from scripts loaded from my domain over https.
Content-Security-Policy: default-src 'none'; script-src https://scotthelme.co.uk; style-src 'unsafe-inline' would be the same as above with the addition of inline CSS.
It's worth noting that you can't specify a directive twice, as the second will be ignored.
Content-Security-Policy: script-src scotthelme.co.uk; script-src google.com would result in no scripts being loaded from google.com. The correct policy would be:
Content-Security-Policy: script-src scotthelme.co.uk google.com
There is also no inheritance from the default source directive.
Content-Security-Policy: default-src https:; script-src http://scotthelme.co.uk would result in no scripts being loaded over https from scotthelme.co.uk. The correct policy would be:
Content-Security-Policy: default-src https:; script-src https://scotthelme.co.uk http://scotthelme.co.uk
Testing A Policy
Once you've created your policy, there's a really great feature you can take advantage of to test it. Instead of sending the header
Content-Security-Policy:, you can send
Content-Security-Policy-Report-Only:. This means the browser will receive and act upon the policy, but instead of enforcing it, it will give you feedback on what the effects of the policy would have been.
Now you can deploy, test and fine tune your policy without risking a disaster like accidentally blocking all JS on the page! Another great feature to take advantage of at this point is the ability to send both headers. Once you're happy with your policy and are sending the
Content-Security-Policy: header, there will no doubt come a time when you want to make changes to that policy. What you can do is implement the changes you need and send the new policy in the
Content-Security-Policy-Report-Only: header. The browser will continue to enforce the existing policy but you can also test and get feedback on the changes from the new policy without having any negative impact. I should probably also mention that CSP is only enforced on a per page basis. The browser will not cache a CSP header and continue to enforce it so you must send the header with every response that you want the policy to be enforced on.
It's all good and well having these policies in place, but it would be even better if we knew when the browser was taking action based on them. Perhaps an asset is being referenced in production that shouldn't be, or perhaps an attacker has found an XSS attack vector and is actively trying to exploit it. Without any form of active reporting, we have no way of knowing what's happening. For this, we have the
report-uri directive. This directive specifies a location that the browser should POST a JSON formatted violation report to in the event it has to take action based on the CSP.
If I used a basic policy on my site to only allow assets to be loaded over https, but a reference to a http asset managed to find a way in, the browser would block the asset from being loaded and deliver a report to me.
Content-Security-Policy: default-src https:; report-uri https://report.scotthelme.co.uk
The offending item:
The resulting report:
"violated-directive": "default-src https:",
"original-policy": "default-src https:; report-uri https://report.scotthelme.co.uk",
Breaking down the values in the report we have:
document-uri: The page the violation occured on.
referrer: The page's referrer.
blocked-uri: The origin of the blocked URI.
violated-directive: The specific directive that was violated.
original-policy: The entire policy.
A Word Of Caution
Though I haven't ventured much into the area of CSP Violation Reporting, I'd say that the implementation of such a feature would need to be approached with great care. The authenticity of incoming reports can never be assured and their contents could well be maliciously crafted by an attacker themselves. Any of the URI values in the report may link to malicious content, and visiting a unique URI could divulge information about the admin that received the report. It would also be highly susceptible to fuzzing and DDoS attacks. Implemented correctly and safely, violation reports can be a source of valuable data. Implemented incorrectly they could potentially do a great deal of harm.
Real World Limitations
Due to CSP working on the basis of whitelisting origins for content, using inline JS or CSS is considered as unsafe. The very foundation of an XSS attack is when an attacker finds a way to inject script into the page. Your typical XSS POC would look something like
alert('XSS') and being inline in the page, the browser can't be sure whether to run the script. For this reason, inline JS and CSS are blocked by default and we have the
unsafe-inline directive to allow it. If you don't allow inline JS, an attacker would be forced to try and load a script resource from another domain and then hit problems as they aren't in the whitelist. To be able to accommodate this, you need to externalise the content of any
<script> tags. Instead of a block of code for your Google Analytics, you would have to create an external file and reference it like
<script src="/assets/js/ga.min.js"></script> instead. That's not such a big problem, and I've externalised all of my JS and CSS. This also means inline event handlers like
onClick"doStuff();" have to go and be replaced with
addEventListener() calls instead. Still, not too much of an issue. That is, until something you depend on uses inline JS or CSS like your blogging platform, a plugin or a library.
My Disqus comment system includes the use of inline CSS and my advertising system uses inline JS too. If I use the
unsafe-inline directive to allow these, I reduce the effectivenes of my CSP so the only other options are to use a hash or a nonce. The basic premise of these two methods is to allow the use of inline scripts or styles but to still control them with the CSP.
The policy to utilise a nonce value would look something like this:
Content-Security-Policy: script-src 'self' 'nonce-SomeRandomNonce'
Any inline script on the page that you wanted to be executed would use that value like so:
This method is more suited to pages that are generated dynamically and requires the use of a sufficiently random and long nonce to prevent an attacker predicting the value for their own scripts. You can use the same nonce value for every script on the page, but it has to be re-generated and inserted into the header and all script elements on each response. A static nonce value would be useless.
The next method is to place a hash of the script or style in the CSP header. More suited to static content, the browser will hash any inline JS or CSS and see if the digest matches a value found in the header. If it does, the content is safe for use.
Content-Security-Policy: script-src 'sha256-HashDigestHere='
The contents of any
<script> tags on the page would then be hashed and compared to the value found in the CSP header. If the values matched, the script would be allowed to execute.
As both of these scripts load 3rd party content, I can't go down the hash route as everything would break as soon as they made the tiniest change to any of the content. This leaves me with the nonce approach. Being quite difficult to implement and maintain, I'm effectively left with the choice of not having adverts and Disqus, or reducing the constraints the policy applies. For the time being, that effectively leaves me with the following policy:
Content-Security-Policy-Report-Only: default-src https: 'unsafe-inline' 'unsafe-eval'
All this does is ensure that all content on the page will be loaded over a secure connection, or not loaded at all, preventing any mixed content warnings. It doesn't offer any protection against XSS. I think this highlights some of the difficulties of implementing CSP and that building a site with CSP in mind from the beginning would be much better. A lot of the requirements, like externalising JS and CSS, are good to implement anyway, outside of the need for CSP. For now though, whilst I have a CSP in place, I would have much preferred something more secure.
Fortunately, support for the CSP header is widespread but there is one thing to watch out for, good old Internet Explorer. The
Content-Security-Policy header is supported in the latest and greatest versions of Chrome, FireFox, Safari (OSX and iOS), Opera (but not Mini), the Android Browser and Chrome for Android. Internet Explorer, however, requires the
X-Content-Security-Policy header instead. This means that if you want to have the most widespread support for your CSP header, you will need to issue it twice! I have to admit I'm not a great fan of that prospect but hopefully that will change in IE 12.