Configure a Content Security Policy for Django
Add a Content Security Policy to Django: Django 6 built-in CSP settings or django-csp, nonces for inline scripts, report-only rollout, collecting violation reports, and handling third-party scripts.
Problem statement
Django escapes template output by default, but cross-site scripting still happens: a |safe filter in the wrong place, a vulnerable JavaScript dependency, user HTML rendered by a rich text field, or an injected third-party script. A Content Security Policy (CSP) is a response header that tells the browser which sources of scripts, styles, images, and connections the page may use. Injected script from anywhere else is refused, even if it reaches the page.
The difficulty is rollout: a strict policy applied at once breaks inline scripts, analytics, fonts, and embeds. This guide adds a policy with Django's built-in support (Django 6.0 and newer) or the django-csp package, uses nonces for the inline scripts you keep, and rolls out in report-only mode first.
- Start with a strict base:
default-src 'self',object-src 'none',base-uri 'self',frame-ancestors 'self'. - Enable it in report-only mode (
SECURE_CSP_REPORT_ONLYin Django 6.0+, or django-csp's report-only setting). - Collect violation reports through a reporting endpoint for one or two weeks.
- Move inline scripts to static files, or give them a per-request nonce.
- Add only the third-party sources you actually need to the relevant directives.
- Switch to the enforcing header, keep reports on, and set the policy in one place only.
Step-by-step solution
1. Draft a base policy
A starting point for a typical Django site that serves its own static files:
default-src 'self';
script-src 'self' 'nonce-...';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'self';
form-action 'self';
object-src 'none'disables plugins.base-uri 'self'stops injected<base>tags from redirecting relative URLs.frame-ancestorscontrols who may embed your pages (the modern replacement forX-Frame-Options).- Add CDN, analytics, or media hosts only to the directives that need them (for example your media bucket in
img-src).
2a. Django 6.0 and newer: built-in CSP
# settings.py
from django.utils.csp import CSP
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.middleware.csp.ContentSecurityPolicyMiddleware",
# ...
]
SECURE_CSP_REPORT_ONLY = {
"default-src": [CSP.SELF],
"script-src": [CSP.SELF, CSP.NONCE],
"style-src": [CSP.SELF],
"img-src": [CSP.SELF, "data:", "https://media.example.com"],
"object-src": [CSP.NONE],
"base-uri": [CSP.SELF],
"frame-ancestors": [CSP.SELF],
"form-action": [CSP.SELF],
"report-uri": ["/csp-report/"],
}
TEMPLATES[0]["OPTIONS"]["context_processors"] += ["django.template.context_processors.csp"]
SECURE_CSP_REPORT_ONLY sends Content-Security-Policy-Report-Only; SECURE_CSP sends the enforcing header. CSP.NONCE inserts a per-request nonce, and the csp context processor makes it available in templates as csp_nonce. Per-view adjustments are possible with the decorators in django.views.decorators.csp. Check the Django documentation for your exact version, since the feature is new.
2b. Older Django: django-csp
pip install django-csp
MIDDLEWARE += ["csp.middleware.CSPMiddleware"]
from csp.constants import NONCE, NONE, SELF
CONTENT_SECURITY_POLICY_REPORT_ONLY = {
"DIRECTIVES": {
"default-src": [SELF],
"script-src": [SELF, NONCE],
"img-src": [SELF, "data:", "https://media.example.com"],
"object-src": [NONE],
"base-uri": [SELF],
"frame-ancestors": [SELF],
"report-uri": "/csp-report/",
}
}
django-csp 4.x uses these dictionary settings; older 3.x releases used separate CSP_* settings. The nonce is available as request.csp_nonce.
3. Use nonces for inline scripts you keep
<script nonce="{{ csp_nonce }}">
document.documentElement.classList.add("js");
</script>
With django-csp, use nonce="{{ request.csp_nonce }}". Better still, remove inline scripts:
- Move code into static
.jsfiles (allowed by'self'). - Pass data with
{{ value|json_script:"id" }}, which renders a<script type="application/json">element that is data, not code, and read it withJSON.parse(document.getElementById("id").textContent). - Replace inline event handlers (
onclick="...") withaddEventListenerin a static file; nonces do not apply to them.
Responses with nonces differ per request, so do not cache full HTML pages in a shared cache unless the nonce is handled accordingly.
4. Collect reports
A minimal endpoint that logs violations:
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("security.csp")
@csrf_exempt
@require_POST
def csp_report(request):
if len(request.body) < 20_000:
try:
report = json.loads(request.body).get("csp-report", {})
logger.warning(
"csp violation directive=%s blocked=%s page=%s",
report.get("violated-directive"), report.get("blocked-uri"), report.get("document-uri"),
)
except (ValueError, AttributeError):
pass
return HttpResponse(status=204)
Browsers send reports as application/csp-report JSON to the report-uri. Reports are noisy (browser extensions inject scripts too); look for patterns across many users rather than individual reports. Hosted reporting services and error trackers such as Sentry can also receive CSP reports. Rate limit the endpoint at the proxy.
5. Adjust, then enforce
For each recurring violation, decide: remove the resource, host it yourself, add its exact host to the right directive, or give the inline code a nonce. Avoid 'unsafe-inline' in script-src: it disables most of the protection. For inline styles from third-party widgets, 'unsafe-inline' in style-src is a common, lower-risk compromise.
Once reports are quiet, switch the setting from report-only to enforcing (SECURE_CSP, or CONTENT_SECURITY_POLICY). You can send both headers at once, enforcing a known-good policy while testing a stricter one in report-only mode.
6. Set the header in one place
If Nginx also adds a Content-Security-Policy header, browsers enforce both, and the effective policy is their intersection, which blocks things neither policy alone would. Generate CSP only in Django (where nonces live) and remove any CSP add_header from Nginx. Large policies also add to response header size.
Verification check:
curl -sI https://example.com/ | grep -i "content-security-policy"
Load key pages, the admin, forms, and payment flows with the browser console open: no CSP errors in enforcing mode, and nonce values that change on every reload.
Explanation
A CSP turns the browser into an enforcement point: every script, style, image, connection, and frame is checked against the policy before loading. Injected markup that tries to load or run code from an unapproved source is blocked, which limits the damage of XSS bugs you have not found yet. Nonces let specific inline scripts run by marking them with a random value that an attacker cannot predict. Report-only mode exists because real pages load more than anyone remembers, and discovering that from violation reports is safer than from broken pages.
Edge cases / notes
Django admin
Test the admin under the enforcing policy, including pages with widgets (date pickers, autocomplete, inline forms). Third-party admin themes and packages may use inline scripts or styles that need exceptions.
Analytics and tag managers
Tag managers load arbitrary scripts by design, which conflicts with a strict script-src. Either list the specific hosts they need, or use 'strict-dynamic' with nonces so scripts loaded by a trusted (nonced) script are allowed.
When to automate this
Add a test asserting the CSP header is present with key directives, and keep a report-only candidate policy running in production so tightening the policy is always based on current data.
Next steps
- Review all headers with the Django Security Headers Checklist.
- Remove insecure resources first with Fix Mixed Content Warnings on a Django HTTPS Site.
- Cover the basics with Django Deployment Security Basics You Should Not Skip.
- If headers grow too large for the proxy, see Fix Nginx "upstream sent too big header" with Django.
FAQ
Does Django have built-in Content Security Policy support?
Yes, from Django 6.0, with ContentSecurityPolicyMiddleware, the SECURE_CSP and SECURE_CSP_REPORT_ONLY settings, and nonce support. Older versions use the django-csp package.
How do I allow an inline script with CSP in Django?
Add a nonce: include the nonce source in script-src and render nonce="{{ csp_nonce }}" (built-in) or nonce="{{ request.csp_nonce }}" (django-csp) on the script tag.
Should I start CSP in report-only mode?
Yes. Report-only shows what would be blocked without breaking pages. Enforce after reviewing reports.
Can I set CSP in both Nginx and Django?
Avoid it. Browsers enforce every policy header, so two policies combine restrictively. Set CSP in one place, preferably Django.