Skip to content

Content Security Policy — three different answers

Severity: mediumBehaviour changed in Django 6.0Applies to: Django 6.0 (built in)Applies to: Django 4.2–5.2 (django-csp 4.0)Facts last verified 2026-07-26 against Django 6.0 · django-csp 4.0

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:

Terminal window
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.0CONTENT_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.

the fix (Django 6.0+)
settings.py
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.

the fix (django-csp 4.0)
settings.py
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.

verify it workedRun this in: http response
Terminal window
curl -sI https://example.com/ | grep -i content-security-policy

Check 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.

if you see this
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.E026
Your 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.W027
before you ship this

CSP 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.