How to Secure Your Netlify Site With a Content Security Policy (CSP)

Learn how to create a flexible and bullet-proof CSP for your Netlify site

in , 11 min read

Note: This article is aimed at Netlify users serving static or pre-rendered content. I will not be discussing CSP rules that bypass best practices or could otherwise be considered ineffective. You can learn more about the full set of available CSP rules at the resources listed in the Further Reading section.

Introduction #

To newcomers, Content Security Policy (CSP) might look a bit intimidating. If you don't understand it, you can easily get it wrong and break your site 😱, or at the very least implement a policy so lax that it fails to provide any meaningful security to your visitors.

Perhaps because of these risks, most websites still don't use any form of CSP - a whole decade after the standard was first published by the W3C. As of 2020, and despite all major browsers supporting it, only 6.3% of websites had a CSP header present.

What is CSP? #

CSP (Content Security Policy) is a method used to detect and mitigate certain types of common website-related attacks, for example: XSS attacks, clickjacking, and data injections.

A CSP most commonly takes the form of this HTTP response header:

Content-Security-Policy

It works by telling browsers that support it which dynamic resources (scripts, styles, images, fonts, and other content) are allowed to load.

CSP is designed to be fully backward compatible. Browsers that don't support it still work with servers that implement it, and vice versa. Browsers that don't support CSP ignore it and default to the standard same-origin policy for web content.

Why Do We Need CSP? #

Cross-Site Scripting (XSS) and data injection attacks are used for everything from data theft, to site defacement, to malware distribution. CSP is an added layer of security that helps to detect and mitigate these types of attacks.

While static or pre-rendered HTML sites hosted by Netlify or other CDNs do offer security gains over server-rendered sites, they are still susceptible to certain types of attack; for example, Reflected XSS.

But arguably, CSP's advantages are not just about security. By defining effective Content Security Policies for our Netlify and Jamstack sites, we learn about which resources we really need - and those that we can probably do without. By keeping such a close watch over what we allow the browser to load, we can get a better understanding of our sites, their performance, and ultimately the experience we're giving our users.

Challenges #

CSP does come with some challenges and risks, primarily those related to configuration and maintenance.

There is a risk of accidentally blocking a resource if you forget to allow it in your CSP. Automatically generating the CSP on each build based on the actual contents of each page can help avoid this happening. See Use a Netlify Build Plugin.

A Basic CSP Example #

Take a look at this basic CSP header:

Content-Security-Policy: default-src 'self'

Although short, this CSP is quite strict. It is telling the browser: "Any type of dynamic content can be loaded but only if it is served from this domain". That means all scripts, CSS, images, and other assets that are not loaded from the site’s domain will be blocked by the browser.

This example is sometimes known as a fallback policy. It allows you to specify the default or fallback resources that can be loaded (or fetched) on the page (such as script-src, or style-src, etc).

It will also disallow inline scripts, inline styles, and resources loaded via the data: scheme e.g. Base64 encoded images and fonts. You'll need to explicitly allow those in your CSP, and in the case of inline scripts and styles, provide a way for the browser to verify that their content has not been manipulated.

CSP is intentionally restrictive by default. Similar to a whitelist, you need to explicitly define the content and sources that you want to be allowed.

Some Common Use Case CSP Examples #

Let's take a look at some common resource loading scenarios, and the corresponding CSP you'd need for each of them.

All Resources Should Load From Your Domain #

This is the basic example or "fallback" that we looked at earlier. This should probably be the starting point for all CSPs that you create. It restricts all resources to your domain, and blocks inline scripts and styles.

Content-Security-Policy: default-src 'self'

This results in:

<script src="/assets/js/myscript.js"> // Allowed
<script src="https://www.mywebsite.com/assets/js/myscript.js"> // Allowed
<script src="https://www.notmywebsite.com/scripts/script.js"> // Blocked
<style>p { color: red }</style> // Blocked

All Resources Should Load From Your Domain Except Google Fonts #

In this example we'll only permit loading of resources from our own domain, except for the Google Fonts service. Because Google Fonts loads the font file from within a stylesheet, we need to specify at least two CSP directives, the style-src and the font-src directive. And because we specify the default-src 'self'; directive at the beginning, almost all other resource types will fallback to this default.

Content-Security-Policy: default-src 'self';font-src fonts.gstatic.com;style-src 'self' fonts.googleapis.com

This results in:

<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet"> // Allowed
<link rel="preload" href="https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu72xKKTU1Kvnz.woff2" as="font" type="font/woff2"> // Allowed
<link rel="preload" href="/assets/fonts/my-font.woff2" as="font" type="font/woff2"> // Blocked

You can see in the last example above that the font we tried to load from our own domain using a relative path was blocked. Why?

Although we began our CSP with the default-src 'self'; "fallback" directive, we overwrote this default for fonts when we added our custom font-src directive, and didn't add 'self' as another valid source.

All Resources Should Load From Your Domain Except Cloudinary Images #

In this example we'll only permit loading of resources from our own domain, except for images served by Cloudinary. And because we specify the default-src 'self'; directive at the beginning, almost all other resource types will fallback to this default.

Content-Security-Policy: default-src 'self';img-src 'self' res.cloudinary.com;

This results in:

<img src="/assets/images/sample.jpg" alt="A flower"> // Allowed
<img src="https://res.cloudinary.com/demo/image/upload/sample.jpg" alt="A flower"> // Allowed
<img src="https://images.unsplash.com/photo-1667063230698-40d2e40b89f1" alt="A bird"> // Blocked

In the last example above, the Unsplash image was blocked because it was neither located at our domain nor at the Cloudinary domain we allowed in our image-src directive.

Block All Scripts #

As its name suggests, this CSP will block all scripts from loading on your site, including those from your own domain. And because we specify the default-src 'self'; directive at the beginning, any other resource types will fallback to the default, same-domain policy.

Content-Security-Policy: default-src 'self';script-src 'none'

This predictably results in:

<script src="/assets/js/myscript.js"> // Blocked
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.1/jquery.min.js"> // Blocked
<script>runInlineScript();</script> // Blocked
<a href="#" onClick="runInlineScript();">Click Me</a> // Blocked

Take a look at some other common use case CSP recipes.

Inline Scripts and Styles #

A big and well known caveat (or feature) of CSP is that it will block all inline scripts and styles by default. CSP only permits scripts and styles with a correct hash of their contents or a nonce value generated at request time. In this way, attackers can't execute a script without knowing the correct nonce or hash for a given response.

From a security perspective this is great; inline scripts and styles are a major attack vector, and blocking them by default is good practice. But it's also a shame because providing your page's critical CSS inside an inline <style> tag in the <head> is an effective performance optimization.

But good news! There are a couple of ways that CSP will allow inline scripts and styles safely.

Allow Inline Scripts and Styles using a Nonce #

You can allow inline scripts and styles by generating a random nonce on each HTTP request, and adding it to the script or style tag nonce attribute. If the browser sees that the nonce matches the value sent in the header, it will allow the script or style to load.

<script nonce="random-83h8he8">
  doSomething();
</script>

But hang on! If your page is served by Netlify, it typically won't be built on the server per request so you can't generate a nonce. Instead, you'll need to use the second method: generate a hash from the script or style tag's content and add it to your CSP header at build time. Read on...

Allow Inline Scripts and Styles using a Hash #

CSP will allow an inline script or style to execute if a computed hash of its contents matches the hash value you send in the header. For example:

Content-Security-Policy: script-src 'self' 'sha256-qznLcsROx4GACP2dm0UCKCzCG-HiZ1guq6ZZDob_Tng='

This results in:

<script src="/assets/js/myscript.js"> // Allowed
<script>alert('Hello, world.');</script> // Allowed because hash matches content
<script>runInlineScript();</script> // Blocked, hash doesn't match

OK that's great, but how do we generate a SHA256 hash from our inline scripts or styles each time it changes i.e. how do we keep the hash up to date?

Generating Hashes From Inline Content #

There are a number of ways to generate a hash value from your inline scripts and styles.

If you already have a CSP configured on your site and it is blocking an inline script or style, you can open the browser developer tools console and it will output what the expected hash of your script/style was in the console error message. You can then copy it and add it (inside single quotes) to your CSP directive.

You can use common tools such as openssl to generate a hash.

echo -n 'runInlineScript();' | openssl sha256 -binary | openssl base64

You could also generate the hash in the build step of your static site generator. The popular Go framework, Hugo has an asset processing function for Fingerprinting and SRI (Subresource Integrity) which can generate SHA256 hashes for your site assets.

You could also generate hashes during your Netlify build using a build plugin.

Add a CSP on Netlify #

Netlify makes it pretty easy to add HTTP headers such as CSP to your site.

You have a few ways of doing it:

Method 1: Inside a _headers File #

Add your headers inside a _headers file in your project root folder.

/*
  Content-Security-Policy: default-src 'self';

Method: 2 Inside the netlify.toml File #

Add your headers inside the netlify.toml configuration file.

[[headers]]
  for = "/*"
  [headers.values]
    Content-Security-Policy = "default-src 'self';"

You only need to use one of the above methods. And be aware: any headers found inside the netlify.toml configuration file will take priority over any found in the _headers file.

Method 3: Use a Netlify Build Plugin #

In my opinion, the most reliable way to define a CSP and keep it up-to-date is to automatically generate it on each build using a Netlify Build Plugin. This gives you access to Netlify's build process, where the plugin can scan your built pages for you and generate the CSP header based on what it finds.

The netlify-plugin-csp-generator plugin generates a CSP for you based on your desired configuration, and it will detect your dynamic content and generate the required header inside netlify.toml.

More importantly, it can generate SHA256 hashes from any inline scripts and styles you might have on your pages - something that would be prone to error and a pain to do manually every time they changed.

Install the netlify-plugin-csp-generator plugin with NPM:

npm install netlify-plugin-csp-generator

Inside your netlify.toml file, add the plugin:

[[plugins]]
package = "netlify-plugin-csp-generator"

  [plugins.inputs]
  buildDir = "dist"

  [plugins.inputs.policies]
    defaultSrc = "'self'"

On building, the plugin will generate a CSP with the "fallback" default-src: 'self'; policy we looked at earlier.

That could be enough for your needs, but you will likely need something a little more involved.

You can learn more about the various configuration options in the plugin README.

Testing CSP #

Using CSP with Content-Security-Policy-Report-Only

If you want to test your CSP but not have the rules enforced by the browser (potentially breaking your site) you can use the Content-Security-Policy-Report-Only header. Any resources that the CSP would normally block will be logged in the console so that you can debug them, but they will still be loaded by the browser - and not break your site.

Once you've finished testing you can rename the header back to Content-Security-Policy, re-deploy your site, and the CSP will be actively enforced.

The netlify-plugin-csp-generator plugin has a handy property that will do this for you. See Use a Netlify Build Plugin

Taking CSP to The Next Level #

There are ways you can make CSP even more useful that are outside the scope of this article. For example, it's possible to configure reporting of CSP violations, sending them to a collection endpoint for further analysis. Or as an ultimate form of protection, you could opt to disallow all script execution.

CSP is still being developed, so new directives continue to be proposed and added. Expect to hear more about CSP (and to be using it a lot more) in the future!

Wrapping up #

In this article I've talked a bit about CSP and shown you how you can implement it on your Netlify sites. There is a lot more to CSP outside the scope of this article, so I strongly recommend checking out the resources below to learn more about this powerful security tool.

If you liked this post or if there is anything you think I could do to improve it, feel free to reach out to me on Twitter or send me an email.

Further Reading #