Improving Web Security with the Content Security Policy
Content Security Policy (CSP) is a security mechanism that helps protect against content injection attacks, such as Cross Site Scripting (XSS). It's a declarative policy that lets you give the browser a whitelist of where it can load resources from, whether the browser can use inline styles or scripts, and whether it can use dynamic JavaScript evaluation—such as through the use of eval
. If there's an attempt to load a resource from somewhere that isn't on this whitelist, loading of that resource is blocked.
How it Works
CSP is currently a Candidate Recommendation published by the W3C WebApplication Security Working Group. It's delivered to the browser via the Content-Security-Policy
HTTP header, which contains one or more directives that whitelist domains from which the browser is allowed to load resources. CSP 1.0 has the following directives:
default-src
script-src
object-src
style-src
img-src
media-src
frame-src
font-src
connect-src
The default-src
, as the name suggests, sets the default source list for the remaining directives. If a directive isn't explicitly included in the CSP header, it will fall back to using the values in the default-src
list.
All directives follow the same pattern:
self
is used to refer to the current domain- one or more URLs can be specified in a space-separated list
none
indicates that nothing should be loaded for a given directive e.g.object-src 'none'
indicates that no plugins—such as Flash or Java—should be loaded.
At its simplest, we could define a CSP to load resources only from the current domain as follows:
Content-Security-Policy: default-src 'self';
If an attempt to load a resource from any other domain is made, it is blocked by the browser, and a message is logged to the console:
By default, too, CSP restricts the execution of JavaScript by disallowing inline scripts and dynamic code evaluation. This, combined with whitelisting where resources can be loaded from, goes a long way to preventing content injection attacks. For example, an XSS attack attempt to inject an inline script tag would be blocked:
As too would any attempt to load an external script that wasn't included in the CSP:
Paths aren't currently supported in the URLs, so you can't lock down your site to only serve CSS from http://cdn.example.com/css. You have to make do with specifying the domain only—for example, http://cdn.example.com. You can, however, use wildcards—for example, to specify all subdomains of a given domain, such as *.mycdn.com.
Subsequent directives don't inherit their rules from previous directives. Each directive you include in the CSP header must explicitly list the domains / subdomains it allows. Here default-src
and style-src
both include self
, and script-src
and style-src
both contain http://cdn.example.com:
Content-Security-Policy: default-src 'self';
style-src 'self' http://cdn.example.com;
script-src http://cdn.example.com;
If you need to use data
URLs for loading resources, you'll need to include data:
in your directive—for example, img-src 'data:';
.
Aside from listing domains, two further features supported by script-src
and style-src
are unsafe-inline
and unsafe-eval
:
unsafe-inline
can be used bystyle-src
andscript-src
to indicate that inline <style> and <script> tags are allowed. CSP uses an opt-in policy. That is, if you don't includeunsafe-inline
, then all inline <style> and <script> tags are blocked.unsafe-inline
also permits inlinestyle
attributes for CSS, and permits inline event handlers (onclick, onmouseover etc.) andjavascript:
URLs (such as<a href="javascript:foobar()">
).unsafe-eval
can be used byscript-src
. Again, it uses an opt-in policy, so if yourscript-src
doesn't explicitly includeunsafe-eval
, any dynamic code evaluation—which includes the use ofeval
, the Function constructor, and passing strings tosetTimeout
andsetInterval
—is blocked.
Browser Support
Browser support for CSP 1.0 is pretty good, with Internet Explorer being the usual elephant in the room: IE10 and IE11 have partial support for CSP via the X-Content-Security-Policy
header, but even then they only appear to support the optional sandbox
directive.
Capturing CSP Violations with report-uri
I mentioned earlier that any violation of your CSP will be logged to the browser console. Whilst that may be fine when your site is under development, it's not really practical when you deploy your CSP to production.
Instead, you can use the report-uri
to log all CSP violations. This directive takes a URL as its value, and makes an HTTP POST request to this URL when a CSP violation is detected. The request body contains a JSON object that is populated with details of the violation.
To illustrate this, suppose we have a CSP as follows:
Content-Security-Policy: default-src 'self';
report-uri: https://example.com/csp/report;
This means the browser is only permitted to load resources from our own domain. However, our site uses Google Analytics, so it attempts to load JavaScript from
www.google-analytics.com. This violates our CSP, so the following JSON is submitted via a HTTP POST request to our report-uri
:
{
"csp-report": {
"blocked-uri:" "https://ajax.googleapis.com"
"document-uri:" "http://example.com/index.html"
"original-policy": "default-src 'self'; report-uri http://example.com/csp/report"
"referrer:" ""
"violated-directive": "default-src 'self'"
}
}
Content-Security-Policy-Report-Only
If you're thinking of implementing CSP, you can take your CSP for a dry run by using the Content-Security-Policy-Report-Only
HTTP header instead of Content-Security-Policy
. This works just the same way as the CSP header, but it only reports on violations without actually enforcing the policy by blocking restricted resources. You can even use both headers at the same time, enforcing one policy while monitoring the effect any changes might have in the other.
Setting the CSP HTTP Header
I mentioned earlier that CSP is sent an HTTP header. Setting HTTP headers can be done directly on the server in your server's configuration file(s):
# Apache config
Header set Content-Security-Policy "default-src 'self';"
# IIS Web.config
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Content-Security-Policy" value="default-src 'self';" />
</customHeaders>
</httpProtocol>
</system.webServer>
# nginx conf file
add_header Content-Security-Policy "default-src 'self';";
Alternatively, many programming languages / frameworks support adding HTTP headers programmatically—such as PHP's header, or Node's setHeader—so you could use these to set your CSP header:
# PHP example
header("Content-Security-Policy: default-src 'self'");
# Node.js example
request.setHeader("Content-Security-Policy", "default-src 'self'");
CSP in the Wild
Let’s have a look at how Facebook and Twitter implement CSP. First, here's Facebook (line breaks added for readability):
default-src *;
script-src https://*.facebook.com http://*.facebook.com https://*.fbcdn.net http://*.fbcdn.net *.facebook.net *.google-analytics.com *.virtualearth.net *.google.com 127.0.0.1:* *.spotilocal.com:* 'unsafe-inline' 'unsafe-eval' https://*.akamaihd.net http://*.akamaihd.net *.atlassolutions.com;
style-src * 'unsafe-inline';
connect-src https://*.facebook.com http://*.facebook.com https://*.fbcdn.net http://*.fbcdn.net *.facebook.net *.spotilocal.com:* https://*.akamaihd.net wss://*.facebook.com:* ws://*.facebook.com:* http://*.akamaihd.net https://fb.scanandcleanlocal.com:* *.atlassolutions.com http://attachment.fbsbx.com https://attachment.fbsbx.com;
Note how Facebook makes use of wildcards for both subdomains, as well as port numbers in connect-src
.
Here's the CSP used by Twitter:
default-src https:;
connect-src https:;
font-src https: data:;
frame-src https: twitter:;
frame-ancestors https:;
img-src https: data:;
media-src https:;
object-src https:;
script-src 'unsafe-inline' 'unsafe-eval' https:;
style-src 'unsafe-inline' https:;
report-uri https://twitter.com/i/csp_report?a=NVQWGYLXFVZXO2LGOQ%3D%3D%3D%3D%3D%3D&ro=false;
Notice how the directives all contain https:
, thus enforcing SSL.
Changes Coming in CSP Level 2
CSP Level 2 is currently a Candidate Recommendation, and defines some new directives and security measures:
base-uri
controls whether the document is allowed to manipulate the base URI of the page.child-src
replacesframe-src
.form-action
controls the document's ability to submit HTML forms.frame-ancestors
works like the X-Frame-Options header, by controlling how this document can be embedded in other documents, and is in fact intended to replace this header.plugin-types
controls which specific plugins can be loaded by the page—such as Flash, Java, Silverlight, etc.
The JSON submitted to report-uri
gets a couple of extra fields: effective-directive
contains the name of the directive that was violated; and status-code
, which contains the HTTP status code of the requested resource, or zero if the resource wasn't requested over HTTP.
CSP Level 2 also includes the ability for inline scripts and style sheets to be protected via nonces and hashes.
Protecting Inline Styles and Scripts Using a Nonce
A nonce is just a random string that's generated on the server, included in the CSP header, and also included on an inline script tag. So, our CSP header with a nonce will look something like this:
Content-Security-Policy: default-src 'self';
script-src 'self' 'nonce-Xiojd98a8jd3s9kFiDi29Uijwdu';
When rendering the page, that same nonce needs to be included in the nonce
attribute on the script tag for the inline script to execute:
<script>
console.log("Script won't run as it doesn't contain a nonce attribute");
</script>
<script nonce="Eskdikejidojdk978Ad8jf">
console.log("Script won't run as it has an invalid nonce");
</script>
<script nonce="Xiojd98a8jd3s9kFiDi29Uijwdu">
console.log('Script runs as the nonce matches the nonce in the HTTP header');
</script>
Protecting Inline Styles and Scripts Using a Hash
To use this approach, first you compute the hash of the style or script block on the server and include this in the CSP header in the style-src
or script-src
respectively. Then the browser computes the hash of the style/script block before it renders the page. If the hash computed by the browser matches the one computed on the server, the style/script block is allowed to execute.
Here the CSP header includes the sha256-base64–encoded hash of the string console.log('Hello, SitePoint');
in the script-src
directive:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'sha256-V8ghUBat8RY1nqMBeNQlXGceJ4GMuwYA55n3cYBxxvs=';
When the browser renders the page, it'll compute the hash of each inline script block and compare them against the whitelisted hashes sent in the CSP header. If it finds a match, the script executes. Note that whitespace is significant. This script will be fine:
<script>console.log('Hello, SitePoint');</script>
The hash of these inline scripts won't match the whitelisted value in the header, so none of them will execute:
<script> console.log('Hello, SitePoint');</script>
<script>console.log('Hello, SitePoint'); </script>
<script>console.log('Hello, World');</script>
Conclusion
In this article, we've had a look at CSP 1.0, and how you can use its directives to whitelist where your site can load resources from. We had a look at how you can use report-uri
to collect information on requests that violate your CSP, and how Facebook and Twitter use CSP today. We finished up by looking at some of the changes that are coming in CSP 2.0—in particular, how you can get more protection for your inline styles and scripts using nonces and hashes.
If you have questions about CSP and how it works, please comment below. Have you given CSP a try yet? If so, how did it go?