← All docs

5 Steps to Create the Ideal Content Security Policy

GUIDE

You may have heard of Content Security Policy (CSP) and why it’s so important. By default web browsers will download, process, and execute most JavaScript and CSS resources requested by a website, which may also include malicious scripts that could have been injected via XSS or other attacks.

CSP is a popular and highly effective security defense against such attacks, but we have to be careful with potential side effects. When misused, it can cause damage to a website by blocking legitimate requests and scripts.

In this post, we’ll show you how to create the ideal policy in 5 steps. You might ask, what’s the ideal policy? One that is strict, secure and does not interfere in a website's behavior or appearance.

Ready? Let's get started!

Step 1: Start with the most restrictive Policy

The best way to get started on the process of creating a secure policy is to choose a strict policy and then loosen it up as needed.

This is an example of a recommended strict policy:

Content-Security-Policy:
script-src 'self';
default-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
upgrade-insecure-requests

This policy will reject all scripts, styles, images, and other resources that are not served by itself. This is a great starting point as most websites should trust their own resources.

Add this header to a website, but keep this change local for now, do not add the policy above to a production environment just yet. There is a high chance that it’ll break quite a few elements of the website.

Step 2: Dealing with the low hanging fruits

After adding the above header to the website, you’ll likely notice the page doesn’t look correct and there are dozens of error messages on the browser's console. If you don’t notice anything wrong, congratulations! Your site does not have any external resources or unsafe scripts.

If you do see errors, don’t panic, it’s normal. We’ll fix that soon.

The next step is to apply a fix to the easy violations. If you have any JavaScript, CSS, or image file that is loaded from an external origin, those resources were blocked by the strict policy. It’s now time to add trusted origins to the policy.

Your policy will be based on your site needs, but in the end, it might look like the following one, which includes an allow list for jQuery Script and Bootstrap CSS.

Content-Security-Policy:
script-src 'self' https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js;
style-src 'self' https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css;
default-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
upgrade-insecure-requests

This is a good opportunity to review the usage of external resources. Do you still need those? Is it secure to load an image or script from that other origin? Do you trust that origin? Remember that we don’t have control over the content served by external origins, they can change (or be hacked) without your consent, which would introduce risk to your website. Where possible, combine external resources with Subresource Integrity for extra security.

Step 3: What about inline code?

This is an interesting topic which deserves its own section. It's not always that easy to fix inline code and it often requires more thinking before taking any action. The strict policy we introduced at the beginning of this post will block the execution of any inline JavaScript or CSS.

What does that mean? Let’s start with JavaScript first. Here’s an example of a script that would not execute under that policy.

<script type="text/javascript">
document.body.prepend("This will NOT execute!")
</script>

Inline styles would also not be evaluated.

<style>
div { color: red }
</style>
<div>This text will NOT be red.</div>

In a nutshell, everything is considered unsafe for the browser unless told otherwise. A quick fix for this is to add unsafe-inline to your policy. But as the name indicates, it’s not safe and it defeats the purpose of CSP, which is to protect pages from script injection attack.

There are at least three solutions to this problem that you should be aware of

Resource

The first option is to move inline JavaScript can be moved to a .js file and inline CSS into a .css file. Simple enough, right?

Script Hash

The second option is to allow specific inline scripts to run using a content hash. You can use a tool like this CSP Hasher to generate a hash add to your policy. If we were to allow the script above, we'd have to add sha256-Fz1wGJU9mMe5MJXvnWsbJFBLsG9j1oRUz+IXYNHgeVM= to the policy.

Content-Security-Policy:
  ...
style-src 'self' 'sha256-Fz1wGJU9mMe5MJXvnWsbJFBLsG9j1oRUz+IXYNHgeVM=';
  ...

Script Nonce

The third option and last option is to generate a nonce (single-use random word), add it to the script/style tag and the policy. Something like:

<script type="text/javascript"> nonce="R4ND0M-G3N3R4T3D-W0RD"
document.body.prepend("This will execute!")
</script>
Content-Security-Policy:
  ...
style-src 'self' 'nonce-R4ND0M-G3N3R4T3D-W0RD';
  ...

Note: A nonce string must be randomly generated by the backend on each request. Using a static or well-known nonce is not secure.

Step 4: Is it ready for Production?

The next step is to navigate through your website and cover as many pages as possible. Don’t forget to test all user-interactive elements too, they might trigger one or more CSP violations. Keep your DevTools open all the time and look for error logs on the console related to CSP. For each log, either prevent it from happening by changing your website or add an exception to your policy.

There is little benefit in using a Content Security Policy if it contains catch-all wildcards and unsafe-inline, avoid it whenever possible.

We’re getting close to the end, but the truth must be said, you definitely won’t find all errors during this process and that’s fine, CSP has a safety net: Reporting.

Step 5: Testing the policy in Production

The first thing we have to do before deploying this policy to production is to change from enforce disposition to report. We can do so by using the header name Content-Security-Policy-Report-Only instead of Content-Security-Policy. The difference is that the former won’t block the execution of any resource or script, even if it doesn’t match the policy, all it does is log an error on the console. That is a crucial part of the process as we don’t want any impact while testing the policy in production.

As mentioned above, violations will be logged to the browser's console, but here’s no point in deploying a Report-Only policy if we can’t see the violation reports, that’s where a report collector comes into play. You can either build your own or use a platform like RepointHub. Sign up for a free trial, create a report bucket, and set up the report-uri directive:

Content-Security-Policy-Report-Only:
script-src 'self' https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js;
style-src 'self' https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css;
default-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
report-uri https://yourbucket.ingest.repointhub.com/report

That's it, you're all set now! The policy above won’t have any user impact, but it’ll collect anonymous violation reports directly from their browsers. It’ll also be tested against multiple browsers and environments, something that is quite complicated to achieve when testing a policy in the local environment.

All you have to do now is keep an eye on the reports and iterate on the policy until you feel it’s safe enough to turn into an enforce CSP. This is just a matter of reverting the header name to Content-Security-Policy, but do not remove the report-uri directive. The violation reports will help us understand if any new code is being blocked by the policy.

That’s it! Your website is now secure against a number of attacks after applying a secure Content Security Policy.

We can be your report collector so you don't need to build your own. Sign up for a free trial of RepointHub to get started.Try it free now!