Getting Started with Content Security Policy using Django
Table of Contents
Introduction
Content Security Policy (CSP) is an added layer of security that helps to detect and mitigate certain types of attacks, including Cross Site Scripting (XSS) and data injection attacks. These attacks are used for everything from data theft to site defacement to distribution of malware.
Content Security Policy is an HTTP header sent by a server (Django) to a client (Chrome) that instructs the client how to handle loading resources such as CSS and JavaScript files. Content security policy works by allowing you to whitelist trusted domains and special keywords such as ‘self’. If a resource, such as a JavaScript file, is loaded by an untrusted domain, the web browser prevents the file from executing.
In this tutorial, we’ll start with a very restrictive Content Security Policy and a broken Bootstrap 4 layout. We’ll walk through some real world scenarios where Content Security Policy breaks our website, and we’ll figure out how to fix it. We use Django to demonstrate the Content Security Policy implementation, but the implementation applies to any programming language.
Project Set Up
- Clone the github repo
git clone https://github.com/laactech/django-csp-example.git
- Create a virtual environment
python3 -m venv csp_venv
- Activate the virtual environment
source ./csp_venv/bin/activate
- Install the requirements
pip install -r requirements.txt
- Migrate the database
./manage.py migrate
- Run the development server
./manage.py runserver
Walk through
Once we set up the project and have it running, navigate to the home page at localhost:8000. As you can see, the home page looks off and doesn’t seem to be using Bootstrap.
Let’s start by opening up the developer tools for our web browser. For Chrome, go to More Tools > Developer Tools > Console. The developer console should display numerous Content Security Policy issues which specifically relate to our Bootstrap CSS and JavaScript.
Let’s also look at the HTTP headers that our development server sends to get an idea about our current Content Security Policy.
As you can see, our current Content Security Policy contains only a default-src with the special value of ’none’ meaning the browser shouldn’t load anything. This prevents all of our Bootstrap CSS and JavaScript from loading. Let’s fix that.
Fixing Bootstrap
Go to our Django settings file under config/settings.py and navigate to line 125. Our project uses a package called django-csp that Mozilla maintains. This package allows us to define the Content Security Policy in our Django settings and send it via a middleware that we added on line 46 of our settings file.
If we look at the errors in our web browser console, we see that the browser refuses to load our Bootstrap stylesheets and scripts because we defined ’none’ in our default-src. This means that the browser should not load anything from any source. Let’s add the domains for our resources into our script-src and style-src.
CSP_DEFAULT_SRC = ["'none'"]
CSP_SCRIPT_SRC = [
"https://stackpath.bootstrapcdn.com",
"https://cdn.jsdelivr.net",
"https://code.jquery.com"
]
CSP_STYLE_SRC = ["https://stackpath.bootstrapcdn.com"]
The script-src allows loading of JavaScript, and the style-src allows loading of CSS stylesheets. To determine which domains we need to add, look at the base.html under django_csp_example/templates/base.html. In the head of the HTML we see our stylesheet being loaded from stackpath.bootstrapcdn.com on line 10. Similarly, if we look at the bottom of the HTML body starting at line 36, we see three different JavaScript files being loaded from three different domains. All of these domains need to be added to our Content Security Policy to communicate to the browser that they’re safe to load.
Once we add these additional directives and refresh the page, we see that our Bootstrap styling works.
Let’s also send another request to our development server to ensure the content security policy changes took effect.
We see that our Content Security Policy now includes our script-src and our style-src, which allows our Bootstrap resources to load properly. If you checked the browser console, you probably see another error relating to our favicon with a note about img-src. To fix this, we need to add an img-src to our Content Security Policy.
CSP_DEFAULT_SRC = ["'none'"]
CSP_SCRIPT_SRC = [
"https://stackpath.bootstrapcdn.com",
"https://cdn.jsdelivr.net",
"https://code.jquery.com"
]
CSP_STYLE_SRC = ["https://stackpath.bootstrapcdn.com"]
CSP_IMG_SRC = ["'self'"]
Our img-src contains another special keyword ‘self’, which allows loading of resources from the same origin, in our case http://localhost:8000. If you reload the web page and view the browser console, the favicon error disappears. Additionally, if you send another request to the development server, the Content Security Policy now contains img-src.
Allowing the Google Slides Iframe
Let’s navigate to another view, which displays an iframe of Google slides. Click the link on the navbar or go to http://localhost:8000/google-slides/.
As we can see, Content Security Policy blocked the loading of the iframe, which caused it to be blank. If we check the console, we see the frame-src error.
Let’s set up our frame-src to allow the iframe to load.
CSP_DEFAULT_SRC = ["'none'"]
CSP_SCRIPT_SRC = [
"https://stackpath.bootstrapcdn.com",
"https://cdn.jsdelivr.net",
"https://code.jquery.com"
]
CSP_STYLE_SRC = ["https://stackpath.bootstrapcdn.com"]
CSP_IMG_SRC = ["'self'"]
CSP_FRAME_SRC = ["https://docs.google.com"]
If we refresh the page, we see that the slide show displays correctly.
Also, if we check the browser console, the frame-src error disappears. Additionally, if we send a request to our development server, we see the frame-src included in the Content Security Policy.
Integrating Google Analytics
For our final Content Security Policy exercise, let’s attempt to add Google analytics to our website. We need to add the following code snippet to the head of our HTML in django_csp_example/templates/base.html
<script async type="application/javascript"
src="https://www.googletagmanager.com/gtag/js?id=UA-XXXX"></script>
<script type="application/javascript">
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'UA-XXXX');
</script>
After we add these two scripts, let’s navigate to the home page at http://localhost:8000. If we check the browser console, we see the new script errors.
Google provides documentation about using Google analytics and Content Security Policy together. The documentation mentions using a nonce, which django-csp generates for us. Django-csp includes the nonce in the HTTP header and in the HTML. If the nonce in the HTTP header and the nonce attribute on an HTML tag, such as script, match, the browser knows the script is safe to load.
If we look at the django-csp documentation, we need to add a context processor. On line 63, of our config/settings.py, let’s add the context processor.
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'csp.context_processors.nonce'
],
Now we need to tell django-csp to include our nonce in the script-src and add our nonce to our Google analytics script files. Let’s configure django-csp to include our nonce in the script-src and add googletagmanager.com and google-analytics.com to img-src in our config/settings.py. We add these two Google domains to our img-src to allow Google analytics to work.
CSP_DEFAULT_SRC = ["'none'"]
CSP_SCRIPT_SRC = [
"https://stackpath.bootstrapcdn.com",
"https://cdn.jsdelivr.net",
"https://code.jquery.com"
]
CSP_STYLE_SRC = ["https://stackpath.bootstrapcdn.com"]
CSP_IMG_SRC = [
"'self'",
"https://www.googletagmanager.com",
"https://www.google-analytics.com"
]
CSP_FRAME_SRC = ["https://docs.google.com"]
CSP_INCLUDE_NONCE_IN = ["script-src"]
Let’s also add the nonce to our new scripts in our django_csp_example/templates/base.html.
<script nonce="{{ CSP_NONCE }}" async type="application/javascript"
src="https://www.googletagmanager.com/gtag/js?id=UA-XXXX"></script>
<script nonce="{{ CSP_NONCE }}" type="application/javascript">
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'UA-XXXX');
</script>
If we refresh the home page and look at the browser console, the Content Security Policy errors disappear. Also, if we send a request to the development server, we see the nonce included in our script-src.
Reporting Content Security Policy Issues
Maintaining a Content Security Policy on a production website takes time. Content security policy controls more than just our JavaScript and CSS files, and as you make changes to your website, you must adapt your Content Security Policy. Thankfully, part of the content security policy specification includes a reporting mechanism. The reporting mechanism allows us to specify a report-uri. With a report-uri defined, the browser sends an HTTP POST request with a JSON report documenting the content security policy errors.
Content security policy report collection options:
- Implement your own report uri to accept the Content Security Policy report
- Use sentry.io
- Use report-uri.com
Personally, I use Sentry to store my Content Security Policy reports because I also use them to monitor my application errors. Having both these features baked into the same service helps me diagnose issues a lot faster. I’m not affiliated with Sentry. I just really like their service.
Let’s add a fake Content Security Policy report-uri for example purposes.
CSP_DEFAULT_SRC = ["'none'"]
CSP_SCRIPT_SRC = [
"https://stackpath.bootstrapcdn.com",
"https://cdn.jsdelivr.net",
"https://code.jquery.com"
]
CSP_STYLE_SRC = ["https://stackpath.bootstrapcdn.com"]
CSP_IMG_SRC = [
"'self'",
"https://www.googletagmanager.com",
"https://www.google-analytics.com"
]
CSP_FRAME_SRC = ["https://docs.google.com"]
CSP_INCLUDE_NONCE_IN = ["script-src"]
CSP_REPORT_URI = ["http://localhost:8000/fake-report-uri/"]
Now let’s make a request to see if our report-uri gets included.
You might need to make multiple requests to see the report-uri included. If you recall,
we used the rate limiting django-csp middleware,
csp.contrib.rate_limiting.RateLimitedCSPMiddleware
. This means that the report-uri
won’t always be included in the Content Security Policy, which helps reduce the number of
requests sent to our report collection solution.
In addition to adding a report-uri, we can send a report only Content Security Policy header to the browser. This allows us to test a potential Content Security Policy without the browser blocking any resources. If the browser would block a resource, the browser sends a report but continues to load the resource.
CSP_DEFAULT_SRC = ["'none'"]
CSP_SCRIPT_SRC = [
"https://stackpath.bootstrapcdn.com",
"https://cdn.jsdelivr.net",
"https://code.jquery.com"
]
CSP_STYLE_SRC = ["https://stackpath.bootstrapcdn.com"]
CSP_IMG_SRC = [
"'self'",
"https://www.googletagmanager.com",
"https://www.google-analytics.com"
]
CSP_FRAME_SRC = ["https://docs.google.com"]
CSP_INCLUDE_NONCE_IN = ["script-src"]
CSP_REPORT_URI = ["http://localhost:8000/fake-report-uri/"]
CSP_REPORT_ONLY = True
Let’s send another request to our development server and see what’s changed.
If you notice, our development server now sends the HTTP header as Content-Security-Policy-Report-Only
instead of Content-Security-Policy
. This tells the browser to only send reports about
Content Security Policy issues instead of blocking the resources. The report only header
allows us to test a potential Content Security Policy on an existing website without worrying
about breaking the website.
Final Thoughts
When I first started implementing the Content Security Policy header, I tried and failed numerous times. A properly implemented Content Security Policy increases the security of your website, and I hope this helps you get started. Feel free to reach out to me with any comments or questions.