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:
pip install django-csp
Step 2: Configure django-csp in settings.py
Add 'csp.middleware.CSPMiddleware' to your MIDDLEWARE setting:
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
:
# 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:
Directive | Purpose |
---|---|
default-src | Fallback directive for other fetch directives |
script-src | Controls JavaScript sources |
style-src | Controls CSS sources |
img-src | Controls image sources |
font-src | Controls font sources |
connect-src | Controls where JavaScript can connect (fetch, XHR, WebSocket) |
frame-src | Controls sources of frames |
media-src | Controls 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 sourceshttps://example.com
: Allows loading resources from the specified domain*.example.com
: Allows loading resources from any subdomain of example.comdata:
: 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.
# 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
# settings.py
CSP_INCLUDE_NONCE_IN = ['script-src']
Step 2: Use the nonce in your templates
{% 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:
# 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:
# settings.py
CSP_REPORT_URI = '/csp-report/' # Django endpoint to collect reports
Then create a view to handle the reports:
# 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:
# 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:
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:
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:
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:
# 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:
- Control which resources can be loaded and executed on your site
- Provide an additional layer of defense against XSS attacks
- 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
-
Basic Implementation: Add CSP to a simple Django project that only loads resources from its own origin.
-
Third-party Integration: Modify your CSP to allow resources from a CDN like Bootstrap or Font Awesome.
-
Report Analysis: Set up CSP in report-only mode, generate some violations, and write code to analyze the violation reports.
-
Progressive Enhancement: Start with a strict CSP policy (
default-src 'self'
) and gradually modify it to accommodate the resources your site needs. -
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! :)