Django Content Security
Policy Guide: What It Is
and How to Enable It

stackhawk

StackHawk|November 1, 2021

Content Security Policy (CSP) is complex. Read this demystifying post to find out several ways to tackle Content Security Policy in Django.

If you're wondering how to enable content security policy (CSP) in Django, you've come to the right place. In this post, we'll give you some basic information about CSP and how to enable it for a Django app. We're going to dig into the basics of CSP first.

CSP: The Basics

At the most basic level, CSP is delivered in a set of headers. These headers tell a user's browser which content is allowed for the webpage. Scripts from another domain or even injected scripts will be blocked if they aren't allowed by the CSP. To be clear, CSP isn't just about scripts. CSP can define policies for all sorts of content such as images, media, frames, styles, and more. Enabling CSP can be a little tricky, but it's a great way to keep your users more secure while visiting your site. 

What Does the Response Header Look Like?

You can add a CSP header more than once. The header consists of several policy directives that contain the directive and value. Here's an example: 

Content-Security-Policy: frame-src 'none';

This basic policy would essentially block frames from being added to the site. The directive is frame-src and the value of the source is none. We can include allowed sources like so: 

Content-Security-Policy: frame-src https://*.adserver.tech;

This policy directive only allows frames from the adserver.tech domain and all its subdomains. While this may be a useful introduction to the header, there's so much more to CSP. For example, if you have in-line scripts or styles, CSP might accidentally block those from loading.

In-line Scripts and Styles

We'll talk more about these later, but here's an overview. Many websites have in-line scripts and styles. The CSP protocols offer a special way of dealing with these. If you aren't too concerned about script injection, you can skip the protection afforded by CSP. Otherwise, you will need to provide one of the two mechanisms to allow in-line scripts or styles. CSP allows for either a nonce (number used only once) or a hash (such as sha256) of the in-line resources. Moving all scripts and styles to their own files may be easier, but this isn't always possible. Fortunately, there's a Django module to handle in-line scripts and styles. 

At this point, you might be thinking, "This is good to know, but how do I make all this happen?" Let's get to that now. 

Mozilla to the Rescue

Mozilla's django-csp (BSD license) makes our lives easier. It gives us several options for implementing CSP headers. Since this is a Django middleware, it follows the conventions for installing and configuring any Django middleware. You can install it with pip or add it to your requirements file. 

pip install django-csp

Then add csp.middleware.CSPMiddleware to your MIDDLEWARE in the settings.py file. This package includes the following: 

  1. global settings for CSP headers delivered via middleware

  2. decorators for request handlers to control CSP headers per view

  3. nonce generation and tools for using nonces in templates

Let's take a look at how to use each of these goodies.

Django-Content Security Policy Global Settings

Again, since this is Django middleware, you can configure it in settings.py or using configure(). Check the Django docs if you need a refresher. This middleware uses the CSP_ prefix for all configuration keys. Values are typically tuples or lists, but some are strings. Check the django-csp documentation for a complete list of settings. Here's an example of some basic settings: 

CSP_DEFAULT_SRC = ("'self'", "https://polyfill.io")
CSP_STYLE_SRC = ("'unsafe-inline'", "https:")

Those settings would add the following header: 

Content-Security-Policy: default-src 'self' https://polyfill.io; style-src 'unsafe-inline' https:

Pay special attention to self and unsafe-inline. Both are special values that need to be single-quoted in the header. Enclose the single quotes in double quotes as in the example settings shown above.

Alternatives to Django-CSP

There are other ways to set headers at a site level in a Django app. You can always set them on your web server. If, for example, you use NGINX to deliver your Django app, you can set CSP using the add-header directive in the NGINX config. For the above example, you would set it like this: 

add-header Content-Security-Policy default-src 'self' https://polyfill.io; style-src 'unsafe-inline' https:

This way isn't as good as django-CSP, but it can work if you have a master template for your entire site. You can use meta tags to specify the CSP headers. This isn't true for all security headers—HSTS can't be done this way—but CSP headers are allowed in meta tags. Note that you can add this header more than once. They will "stack" and become additive. 

<head>
...
<meta http-equiv="Content-Security-Policy" content="default-src: 'self' https://polyfill.io" />
<meta http-equiv="Content-Security-Policy" content="style-src: 'unsafe-inline' https:" />
...

The markup above will add two CSP headers, which is perfectly acceptable. Using meta tags is helpful for any HTML-based app, not just Django. 

Page-Level Content Security

Content security policies added by django-CSP can be updated or overridden at the page or view level. However, this can be difficult if you use meta tags and could become cumbersome in the NGINX config. And removing a global policy at the page level is not so straightforward. That's because adding another header with the same directive will not remove or replace the old values. 

Here's an example. Let's say you have a site-level CSP that adds the following header that blocks all iframe and frame content: 

Content-Security-Policy: frame-src 'none'

Then you add a page that needs a frame and include a meta tag with the following:

Content-Security-Policy: frame-src https://adserver.com

With the above header, you would expect that page to allow iframes from the ad server. Well...sorry to disappoint, but you'll have to try something else, since the main policy denies all iframes.

Django-Content Security Policy Page-Level Options

With django-CSP, you have some options. The replace decorator is the least destructive option for a page-level policy change. Second, you can add to the policy using the update decorator. You can swap out the header wholesale with the CSP decorator. Or, you can destructively set the page to have no CSP header. Here are some examples of each:

@csp_replace(FRAME_SRC="https://adserver.com")
def the_page(request):

@csp_update(DEFAULT_SRC="https://adserver.com")
def the_page(request):

@csp(FRAME_SRC="https://adserver.com",DEFAULT_SRC="'self'")
def the_page(request):

@csp_exempt
def the_page(request):

def the_page(request):
  response = get_response(request)
  response._csp_exempt = True
  return response

Pure Django Options

Of course, you can set CSP headers on the response object if you prefer. This is especially useful if you need to set CSP headers dynamically. You can set headers on the response object directly or through the headers property if you are using Django 3.2 or higher. Here are examples of each: 

# Django *
response["Content-Security-Policy"] = "default-src 'self' https://polyfill.io"

# Django >= 3.2
response.headers["Content-Security-Policy"] = "default-src 'self' https://polyfill.io"

The code above is useful for setting CSP headers on a page-by-page basis. However, remember that the policies are only additive. The policy set on a page response will not override any global policies set elsewhere, such as in NGINX or through middleware. If you are using django-CSP and you set csp_exempt, you may be able to set a custom policy that only applies your response headers set in code. (This is all theoretical, however, since I have not validated this proposition. If you try it out, please post your results in the comments.) 

Now, let's get back to discussing in-line scripts and styles. 

Handling In-line Scripts and Styles

Handling in-line scripts and styles requires a bit of thought. In Django, you can write these tags in-line in a template, include them dynamically in the template, or add them dynamically in the view. CSP can allow all in-line tags, only allow the ones you control, or block them all. The latter is the most secure, so you may want to think about moving any in-line scripts and styles into their own files. However, it's very common to write in-line scripts for anything from tracking to ads or APM, so we need to know how to deal with that. 

Like we mentioned earlier, you have two options for in-line scripts and styles: you can either use a nonce or use a hash of the script. 

For some in-line scripts, it may serve you well to use a hash of the script. This could be useful for in-line tags that will not change frequently. You can hash the script once and set up the header. For other scripts, especially dynamic scripts, you might be better served using a nonce. 

Hashed Tag

To add in-line scripts using a hash, generate the hash of the script (minus the tags only). Then take that hash, base-64 encode it, and add that to the CSP header. The header will resemble this: 

Content-Security-Policy: script-src 'sha512-QUM2MzAzRjVBRDhENTdFRkM3N0VBNEQ1MjQzMUNGMDIyNjkyNjg0M0Q2RkQ5MjgxMUFEMzY5OUI5QkEyMUE3NDFDNDM2RTU4MjNGMjZGNkJBRjIxQjcxMjE4QzRGNUI1NzU2MUNFNjk5N0RFRTUxMzZBODBFRDMzQzc2NDFBMTQ='

Do that for each in-line script or style tag, and you're set to block possible script injection attacks from scripts from outside your code. Oh and BTW, sorry for breaking out of the layout with the hash ;) they are quite long and cumbersome. Nonces might be more manageable if you need to allow a lot of tags.

Nonce

The django-CSP package is the best way to use a nonce for in-line tags. Its middleware sets a lazy-evaluated nonce on incoming requests. Simply use that in your tags and set the CSP_INCLUDE_NONCE_IN ["script-src", "style-src", ... ] setting; the middleware will take care of setting the response headers. 

<script nonce={{request.csp_nonce}}>...your very useful JavaScript code...</script>

And since the nonce is on the request object, you can also use it in your views code for any dynamically generated tags.

Add Automated Security Testing to Your Pipeline for Free

Conclusion

Adding content security policy headers to your Django app can make it more secure against a number of attack vectors. However, CSP is somewhat complex and cumbersome, depending on your situation. After reading this post, you now have a variety of methods at your disposal that can help you add CSP and level up your security game. But remember that some third-party vendors such as advertisers might not play well with the most secure policies. And, as always, apply this knowledge to your specific context rather than simply using example cases directly. 

This post was written by Phil Vuollet. Phil leads software engineers on the path to high levels of productivity. He writes about topics relevant to technology and business, occasionally gives talks on the same topics, and is a family man who enjoys playing soccer and board games with his children.


StackHawk  |  November 1, 2021