Content Security Policies
~
A good defensive strategy is multilayered. Whether it's the multifactor authentication system you use to log into GitHub, or the kill switch on Furiosa's war rig, having more than one safeguard against intrusion makes attacks substantially more difficult. The same is true for web security, and today's post is going to introduce you to a powerful tool you have to augment your website's security: content security policies, or CSPs.
Introduction
You can think of a content security policy as a bouncer standing in front of your webpage. It's the bouncer's role to check resources against a predetermined guest list and that each guest matches certain entrance criteria. Take, for example, a hypothetical content security policy for this very blog:
Content-Security-Policy: default-src: 'self' cdn.frontendian.co;
I'll explain more about the syntax in a bit–this policy tells the browser to only accept resources that were loaded from the same origin as the page itself, namely frontendian.co
, or the domain cdn.frontendian.co
. But that's not all–just by specifying this policy, we prevent any inline (or evaluated) CSS or JavaScript from being loaded into the page. We could enable either via additional keywords in our policy:
Content-Security-Policy: default-src: 'self' 'unsafe-inline' 'unsafe-eval' cdn.frontendian.co;
By adding those two keywords, we now have a content security policy that communicates three things:
- Only allow external resources from my page's origin.
- Allow inline scripts and stylesheets.
- Allow the use of JavaScript
eval
.
As we'll discuss in a moment, you can still be much more finegrained when it comes to how you want to load different kinds of resources. CSP directives, like default-src
, exist for fonts, scripts, styles, and more, and allow to enforce stricter criteria for one resource type over another.
History
What motivated the creation of content security policies? In short: the rise of sites that allowed user-submitted scripts and styles, such as MySpace. Robert Hansen (perhaps better known as RSnake) has a nice writeup on the origins of CSPs at Mozilla.
As was mentioned above, given the risk that malicious user-submitted content can pose it's best to have multiple layers of defense. Then and now, the primary means of disarming user content is via sanitization, but if that fails you're left without much recourse. CSPs ensure that, even if a user does somehow bypass your sanitization mechanisms, the browser will still prevent the malicious resources from executing.
Crafting a Policy
CSPs are represented as a single line of text, and are communicated to the browser in one of two ways:
- Via a
<meta>
element with itshttp-equiv
attribute set toContent-Security-Policy
, e.g.<meta http-equiv="Content-Security-Policy" content="...">
. - Via a
Content-Security-Policy
header on the HTTP resource. For a handful of reasons which will be discussed, this is the preferred means of communicating a CSP to the browser.
The individual components of a CSP are called directives, and it is through these directives that you instruct the browser how it should treat various resources. Directives begin with the directive's name, such as default-src
or img-src
and are followed by one or more domains or keywords (like 'unsafe-inline'
or 'self'
) which is called the directive's source list.
Fetch Directives
Directives that specify which resources may be loaded into a page are known as "fetch directives". What follows is a brief tour of the available fetch directives and their quirks.
default-src
The default-src
directive specifies the default source list for all other fetch directives, though it can be overriden by specifying another fetch directive in your policy.
default-src: 'self' assets.frontendian.co;
Note that default-src
may also list keywords that apply to a subset of fetch directives, such as 'unsafe-inline'
, given that directives which do not recongize those keywords will simply ignore them.
child-src
The child-src
directive is one of the more confusing directives in the CSP spec. Introduced in the second edition, it was intended to replace the frame-src
directive and govern both "nested browsing contexts" (<iframe>
s and <frame>
s) as well as script resources loaded as Worker
s. However, in the third edition of the spec the worker-src
directive was introduced, and the child-src
directive simply became shorthand for specifying both the frame-src
and worker-src
directives. Oy vey!
Given this, the child-src
directive exists in a sort of limbo. It's unclear from the second edition of the CSP spec whether user agents should prefer a frame-src
directive over the child-src
directive with regard to nested browsing contexts, and so I recommend doing some thorough testing before rolling out the child-src
directive.
child-src: 'self' scripts.frontendian.co;
connect-src
The connect-src
directives governs which resources may be obtained via APIs like XMLHttpPRequest
or fetch
. That's not a complete list, and you're encouraged to check out the full reference on the W3C site.
connect-src: 'self' api.frontendian.co;
font-src
The font-src
directive gates all font resources your page might load, which more than likely originate from @font-face
declarations. The font-src
directive accepts no special keywords beyond 'self'
or 'none'
.
font-src: 'self' fonts.frontendian.co;
frame-src
While deprecated in the second edition of the CSP spec, the frame-src
directive makes its triumphant return in the third. frame-src
governs which URLs may be accessed via "nested browsing contexts", such as <iframe>
s and <frame>
s.
It's recommended that you read up on the child-src
directive if you plan on implementing frame-src
, as it's one of the sharper corners of the CSP spec.
frame-src: 'self' iframes.frontendian.co;
img-src
The img-src
directive gates image resources, and respects no special keywords beyond 'self'
or 'none'
.
img-src: 'self' img.frontendian.co;
manifest-src
The manifest-src
directive, introduced in the third edition of the CSP spec, gates app manifests, which are one of the building blocks of Progressive Web Apps.
manifest-src: 'self' manifest.frontendian.co;
media-src
The media-src
directive gates audio and video resources, and respects no special keywords beyond 'self'
.
media-src: 'self' media.frontendian.co;
object-src
The object-src
directives gates browsing contexts generated by elements such as <object>
or <embed>
.
object-src: 'self' embed.frontendian.co;
script-src
The script-src
directive is arguably the most complex of all CSP directives, and for good reason. Malicious pose tremendous risk to webpages, and sites with a large number of script resources need sophisticated means of ensuring that every <script>
is whitelisted.
To start with, the script-src
directive accepts the standard 'self'
and domain elements for its source list:
script-src: 'self' scripts.frontendian.co;
In addition to these, we also have the 'unsafe-inline'
and 'unsafe-eval'
keywords at our disposal. The former allows the usage of <script>
tags that execute JavaScript inline, while the latter prevents the usage of eval
to evaluate strings into executable JavaScript. If at all possible you should avoid using these keywords.
Yet as many of us who maintain legacy websites know, such a thing just might not be possible. So the script-src
directives gives us a couple neat ways to ensure we only allow whitelisted inline JavaScript.
The first method, called nonce-source
, allows you to specify a "nonce", or random key, that can be used to specify approved inline scripts. For example, consider the following:
script-src: 'nonce-1a2b3c';
<script nonce="1a2b3c">function () { alert('Hello!'); }</script>
Since our script-src
directive specifies the 'nonce-1a2b3c'
nonce-src
, we are able to execute any <script>
that possesses the nonce
attribute set to 1a2b3c
.
The second method, called hash-source
, allows you to take a hash of the script you wish to execute and specify that in your script-src
directive.
script-src: 'sha256-qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng=';
<script>alert('Hello, world.');</script>
Given that the hash of the script (taken as sha256
) matches the hash appended to sha256-
in the script-src
directive's source list, the script will be allowed to run.
style-src
The style-src
directive gates CSS resources, and borrows a couple of the special keywords from the script-src
directive ('unsafe-inline'
and 'unsafe-eval'
).
The 'unsafe-inline'
keyword works as you might expect, and disallows the usage of <style>
elements on the page, as well as style
attributes on DOM nodes. The 'unsafe-eval'
keyword, however, is a bit less intuitive. It disallows interactions with the CSS Object Model, or CSSOM, which equates to being able to mutate a page's styles with JavaScript.
style-src: 'self' styles.frontendian.co;
As an added perk, both the nonce
and hash
sources are available to style-src
, and they are meant to function to apply to <style>
elements in the same way that they would to <script>
elements.
worker-src
The worker-src
directive was introduced in the third edition of the CSP spec, and governs which script resources may be used to create web workers. In the second edition of the CSP spec this responsibility was borne by the child-src
directive, which also governed nested browsing contexts, but the decision was made to split the workload across two directives.
worker-src: 'self' scripts.frontendian.co;
Document Directives
Document directives provide more meta-level guidance about how the document should behave.
base-uri
The base-uri
directive gates which URLs may become the base URL of a page. This helps protect your page against base tag hijacking.
base-uri: 'self';
plugin-types
The plugin-types
directives specifies the sorts of content that may be loaded into <object>
or <embed>
tags. You do this by passing a list of mimetypes as source list, like so:
plugin-types: application/pdf;
Navigation Directives
Navigation directives govern where your page may go after a user action (restricted to <form>
s at the moment), or in which sorts of nested browsing contexts it may live.
form-action
The form-action
directive specifies which URLs a form may send resources or user information. Be aware that, since this directive is considered a navigation directive, the default-src
directive will not supply it with a default value. Make sure to set this directive independently if your site makes use of <form>
s.
form-action: 'self' api.frontendian.co;
frame-ancestors
The frame-ancestors
directive governs which resources may load the page into an <iframe>
or other nested browsing context.
It is intended to obsolete the X-Frame-Options
header.
frame-ancestors: 'none';
Monitoring Your Policy's Effectiveness
One of the biggest advantages of CSPs is that you can monitor attempts to breach your policy in real-time. This is done by specifying a report-uri
(and/or report-to
, per the third edition) directive in your policy.
report-to: https://csp.frontendian.co;
Should a violation of your CSP occur in a user's session, the browser will POST to that URL with an object of the following structure:
{
"document-uri": "https://frontendian.co",
"referrer": "",
"blocked-uri": "http://haxors.com/malicious/script.js",
"violated-directive": "script-src: 'self'",
"original-policy": "script-src: 'self'; report-to: https://csp.frontendian.co;"
}
Be aware that reporting can easily swamp your site if you have a number of visitors and haven't yet properly vetted all of the possible resources that may violate your policy. Make sure to set up your reporting endpoint such that it won't impact your site's resources. Serverless offerings like Zeit or Lambda would be great for this.
Testing Your Policy
As you many have already imagined, a content security policy deployed prematurely can debilitate your site. Particularly if you use third-party scripts, you may be surprised (or perhaps horrified) to learn that those third-party scripts are themselves loading other scripts, and that your CSP is preventing crucial resources from loading.
To account for this, the authors of the CSP spec made an affordance for those wishing to test out their CSPs before fully deploying them. This affordance is known as report-only
mode, and allows you to deploy a content security policy without the browser actually enforcing the policy. Yet the browser will still send reports, as we discussed above, making it an effective way to ensure you've checked all of your boxes before activating your policy.
You can activate report-only
mode by simply communicating your policy to the browser via the Content-Security-Policy-Report-Only
header, instead of Content-Security-Policy
.
Conclusion
CSPs are quickly becoming an essential part of every website's security toolkit. Hopefully this post has given you a better understanding of how you can tailor CSPs to your site's needs, and if you've spotted an error, be it factual or syntactical, let me know in the comments below!