Content Security Policy — three different answers
Almost every Django CSP guide online is correct for a version you are probably not running. There are three configuration formats in circulation and they are mutually incompatible.
Find out which applies before writing any policy:
python -c "import django; print(django.get_version())"pip show django-csp 2>/dev/null | head -2| Your situation | The answer |
|---|---|
| Django 6.0+ | Built into core. SECURE_CSP. No package. |
| Django 4.2–5.2 (incl. the LTS) | django-csp 4.0 — CONTENT_SECURITY_POLICY dict |
Any version, django-csp ≤ 3.8 |
CSP_DEFAULT_SRC etc. — removed in 4.0, will not work |
Note the middle row. 5.2 is the LTS, supported to April 2028, and it does not have built-in CSP. “Django has CSP now” is true and will not help you for years.
Django 6.0 — built in
Section titled “Django 6.0 — built in”from django.utils.csp import CSP
MIDDLEWARE = [ "django.middleware.csp.ContentSecurityPolicyMiddleware", # ...]
SECURE_CSP = { "default-src": [CSP.SELF], "script-src": [CSP.SELF, CSP.NONCE], "img-src": [CSP.SELF, "https:"],}CSP.SELF and CSP.NONCE are sentinels rather than strings — CSP.NONCE is what
makes the middleware generate a per-response nonce. If you use it, you must also
enable the django.template.context_processors.csp context processor, or templates
cannot emit the nonce and every gated script is blocked. That is exactly what
security.W027 catches.
Django 4.2–5.2 — django-csp 4.0
Section titled “Django 4.2–5.2 — django-csp 4.0”CONTENT_SECURITY_POLICY = { "DIRECTIVES": { "default-src": ["'self'"], "script-src": ["'self'"], },}django-csp 4.0 consolidated every CSP_* setting into CONTENT_SECURITY_POLICY and
CONTENT_SECURITY_POLICY_REPORT_ONLY, and its own migration guide states the change
is backwards-incompatible. So a guide showing CSP_DEFAULT_SRC = ("'self'",) is not
merely old — those settings are no longer read at all.
The failure mode is silence. Leftover CSP_* settings do not raise; they are
simply ignored, and the site serves either no policy or the default one. Nothing tells
you the policy you carefully wrote is not in effect.
curl -sI https://example.com/ | grep -i content-security-policyCheck the response header. This is the one control where reading the setting proves nothing, because all three wrong configurations look plausible in a settings file and produce no header.
The Content Security Policy setting '%s' must be a dictionary (got %r instead).SECURE_CSP or SECURE_CSP_REPORT_ONLY was set to something other than a dict — usually a header string copied from a non-Django guide.
Read in: django/core/checks/security/base.py — id security.E026Your Content Security Policy includes CSP.NONCE and 'django.middleware.csp.ContentSecurityPolicyMiddleware' is enabled, but 'django.template.context_processors.csp' is not enabled in your TEMPLATES setting.Nonces are being generated but templates cannot read them, so every nonce-gated script is blocked. The policy is working and the page is broken.
Read in: django/core/checks/security/base.py — id security.W027CSP breaks inline scripts and inline styles — that is the entire point, and it is why a policy written straight into enforcing mode takes the front end down.
Start in report-only (SECURE_CSP_REPORT_ONLY on 6.0,
CONTENT_SECURITY_POLICY_REPORT_ONLY on django-csp), ship it, read what violates,
then move the same policy across. Django’s admin and many third-party widgets emit
inline handlers, so the first report run is always noisier than expected.
Migrating between the three formats is not a rename. Convert deliberately and verify with the header, not the settings file.
Related
Section titled “Related”- Security headers — the headers Django sets without a package
- SECURE_BROWSER_XSS_FILTER was removed — the header CSP replaced