Skip to main content

Django Content Security Policy

Introduction

Content Security Policy (CSP) is a critical security feature that helps protect your Django web applications against cross-site scripting (XSS) attacks and other code injection vulnerabilities. CSP works by allowing you to specify which content sources are considered trusted, and instructs the browser to only execute or render resources from those trusted sources.

In this tutorial, we'll explore:

  • What Content Security Policy is and why it matters
  • How to implement CSP in Django applications
  • Configuring CSP directives for different resources
  • Testing and debugging your CSP implementation
  • Real-world examples and best practices

What is Content Security Policy?

Content Security Policy is an HTTP response header that tells browsers which dynamic resources are allowed to load on a webpage. By implementing CSP, you create a whitelist of sources that the browser should consider valid for executable resources. Any resource that doesn't match this whitelist will be blocked from loading.

CSP can help mitigate various types of attacks, including:

  • Cross-site scripting (XSS) attacks
  • Clickjacking
  • Data injection attacks
  • Unwanted resource loading

Implementing CSP in Django

While Django doesn't include built-in CSP support, we can add this functionality using the django-csp package, which makes implementing CSP straightforward.

Step 1: Install django-csp

First, let's install the package using pip:

bash
pip install django-csp

Step 2: Configure django-csp in settings.py

Add 'csp.middleware.CSPMiddleware' to your MIDDLEWARE setting:

python
MIDDLEWARE = [
# Django's default middleware
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
# ...

# Add CSP middleware - it should be near the top
'csp.middleware.CSPMiddleware',

# Other middleware
# ...
]

Step 3: Define CSP Policies

Now, let's configure the CSP directives in your settings.py:

python
# Content Security Policy settings
CSP_DEFAULT_SRC = ("'self'",) # Allow resources from same origin
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'", "https://fonts.googleapis.com") # Allow styles from self, inline, and Google Fonts
CSP_SCRIPT_SRC = ("'self'", "'unsafe-inline'") # Allow scripts from same origin and inline scripts
CSP_FONT_SRC = ("'self'", "https://fonts.gstatic.com") # Allow fonts from self and Google Fonts
CSP_IMG_SRC = ("'self'", "https://images.unsplash.com", "data:") # Allow images from same origin and Unsplash
CSP_CONNECT_SRC = ("'self'",) # Allow connections to same origin

Understanding CSP Directives

Let's break down the most common CSP directives:

DirectivePurpose
default-srcFallback directive for other fetch directives
script-srcControls JavaScript sources
style-srcControls CSS sources
img-srcControls image sources
font-srcControls font sources
connect-srcControls where JavaScript can connect (fetch, XHR, WebSocket)
frame-srcControls sources of frames
media-srcControls audio and video sources

Common CSP Source Values

  • 'self': The same origin (same scheme, host and port)
  • 'unsafe-inline': Allows inline JavaScript and CSS (not recommended for strict security)
  • 'unsafe-eval': Allows eval() and similar JavaScript functions (avoid when possible)
  • 'none': Does not allow any sources
  • https://example.com: Allows loading resources from the specified domain
  • *.example.com: Allows loading resources from any subdomain of example.com
  • data:: Allows loading resources with the data scheme (often used for images)

Implementing CSP for a Real Django Application

Let's walk through a more realistic example of CSP implementation for a Django blog application that uses Bootstrap, external images, and Google Fonts.

python
# settings.py for a blog application with external dependencies

# Base CSP settings
CSP_DEFAULT_SRC = ("'self'",)

# Script sources - allowing Bootstrap JS from CDN
CSP_SCRIPT_SRC = (
"'self'",
"https://cdn.jsdelivr.net/npm/[email protected]/",
"https://code.jquery.com/",
)

# Style sources - allowing Bootstrap CSS from CDN and Google Fonts
CSP_STYLE_SRC = (
"'self'",
"https://cdn.jsdelivr.net/npm/[email protected]/",
"https://fonts.googleapis.com/",
)

# Font sources for Google Fonts
CSP_FONT_SRC = (
"'self'",
"https://fonts.gstatic.com/",
)

# Image sources - allowing blog post images from various sources
CSP_IMG_SRC = (
"'self'",
"https://images.unsplash.com/",
"https://res.cloudinary.com/",
"data:",
)

# If your blog has embedded content
CSP_FRAME_SRC = (
"'self'",
"https://www.youtube.com/",
"https://player.vimeo.com/",
)

# If you're using AJAX or fetch API
CSP_CONNECT_SRC = (
"'self'",
"https://api.example.com/",
)

Adding Nonce for Inline Scripts

Sometimes you need to use inline scripts but don't want to use the unsafe 'unsafe-inline' directive. For these cases, you can use a nonce (a one-time token):

Step 1: Configure your settings to use a nonce

python
# settings.py
CSP_INCLUDE_NONCE_IN = ['script-src']

Step 2: Use the nonce in your templates

html
{% load csp %}
<!DOCTYPE html>
<html>
<head>
<title>My Django App</title>
</head>
<body>
<!-- Your content here -->

<script nonce="{% csp_nonce 'script-src' %}">
// Your inline JavaScript here
document.getElementById('demo').innerHTML = 'Hello World!';
</script>
</body>
</html>

Testing Your CSP Implementation

Using the Report-Only Mode

When first implementing CSP, it's wise to use the report-only mode, which will report violations but not actually block any resources:

python
# settings.py
CSP_REPORT_ONLY = True # Violations will be reported but not blocked

Setting Up Violation Reports

You can configure where CSP violation reports are sent:

python
# settings.py
CSP_REPORT_URI = '/csp-report/' # Django endpoint to collect reports

Then create a view to handle the reports:

python
# views.py
import json
import logging
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST

logger = logging.getLogger('csp_violations')

@csrf_exempt
@require_POST
def csp_report(request):
"""View to handle CSP violation reports."""
try:
report = json.loads(request.body.decode('utf-8'))
logger.warning(f"CSP Violation: {report.get('csp-report', {})}")
except json.JSONDecodeError:
return HttpResponse(status=400)
return HttpResponse(status=204) # No content response

And add the URL:

python
# urls.py
from django.urls import path
from .views import csp_report

urlpatterns = [
# Your other URLs
path('csp-report/', csp_report, name='csp-report'),
]

Common Challenges and Solutions

Challenge 1: Third-party Scripts

Many sites use third-party scripts like Google Analytics or advertising scripts that might inject their own scripts or styles.

Solution: Add the necessary domains to your CSP directives:

python
CSP_SCRIPT_SRC = (
"'self'",
"https://www.googletagmanager.com/",
"https://www.google-analytics.com/",
)

Challenge 2: Inline Styles in WYSIWYG Content

When using a WYSIWYG editor that generates inline styles.

Solution: Consider using a nonce or hash, or in some cases, you might need to use 'unsafe-inline' for styles:

python
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")

Challenge 3: Frame Loading Issues

If your site embeds content from other sites.

Solution: Specify the allowed sources in the frame-src directive:

python
CSP_FRAME_SRC = ("'self'", "https://www.youtube.com", "https://player.vimeo.com")

Real-world Example: E-commerce Site

Let's look at a more complex example for an e-commerce Django site:

python
# settings.py for an e-commerce site

# Base CSP settings
CSP_DEFAULT_SRC = ("'self'",)

# Script sources - allowing payment processors and analytics
CSP_SCRIPT_SRC = (
"'self'",
"https://js.stripe.com/",
"https://www.paypal.com/",
"https://www.googletagmanager.com/",
"https://www.google-analytics.com/",
)

# Style sources
CSP_STYLE_SRC = (
"'self'",
"https://fonts.googleapis.com/",
)

# Image sources - allowing product images from CDN
CSP_IMG_SRC = (
"'self'",
"https://cdn.ecommerce-products.com/",
"https://res.cloudinary.com/",
"data:",
)

# Connect sources for API calls
CSP_CONNECT_SRC = (
"'self'",
"https://api.stripe.com/",
"https://www.google-analytics.com/",
)

# Frame sources for payment widgets
CSP_FRAME_SRC = (
"'self'",
"https://js.stripe.com/",
"https://www.paypal.com/",
)

# Font sources
CSP_FONT_SRC = (
"'self'",
"https://fonts.gstatic.com/",
)

Summary

Content Security Policy is a powerful security mechanism that can greatly enhance the protection of your Django applications against cross-site scripting and other injection attacks. By properly implementing CSP, you:

  1. Control which resources can be loaded and executed on your site
  2. Provide an additional layer of defense against XSS attacks
  3. Get visibility into potential security issues through violation reports

Although implementing CSP might require some initial configuration and adjustments to accommodate third-party resources, the security benefits are substantial and well worth the effort.

Remember to start with CSP in report-only mode, analyze the violations, and gradually tighten your policy as you address the issues. Eventually, you'll have a robust policy that effectively protects your users without hindering functionality.

Additional Resources and Exercises

Resources

Exercises

  1. Basic Implementation: Add CSP to a simple Django project that only loads resources from its own origin.

  2. Third-party Integration: Modify your CSP to allow resources from a CDN like Bootstrap or Font Awesome.

  3. Report Analysis: Set up CSP in report-only mode, generate some violations, and write code to analyze the violation reports.

  4. Progressive Enhancement: Start with a strict CSP policy (default-src 'self') and gradually modify it to accommodate the resources your site needs.

  5. Nonce Implementation: Practice using nonces for inline scripts instead of using 'unsafe-inline'.

By following these guidelines and exercises, you'll build a solid understanding of Content Security Policy implementation in Django and enhance your application's security posture.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)