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.
What 6.1 changed — the admin stopped breaking
Section titled “What 6.1 changed — the admin stopped breaking”The settings API is unchanged in 6.1: SECURE_CSP and SECURE_CSP_REPORT_ONLY work
exactly as they do in 6.0. What changed is nonce plumbing, and one part of it matters
before you turn a nonce policy on.
Nonce attributes are now added on <script>, <style> and <link> elements in the
admin templates and all built-in templates, when the csp() context processor is
configured. On 6.0 that was your problem — a CSP.NONCE policy would apply to the
Django admin too, and the admin’s own inline assets carried no nonce, so the admin
broke and the fix was to exempt it or patch templates. On 6.1 the built-in templates
carry nonces themselves.
A new csp_nonce_attr template tag renders the nonce attribute on <script> and
<link> elements, or renders a Media object’s assets with the nonce applied — again
only when the csp() context processor is configured. It replaces hand-writing
nonce="{{ request.csp_nonce }}" on every tag.
So if you are on 6.0 and a nonce policy broke your admin, that is a known gap that 6.1 closes rather than something you configured wrong.
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.
SECURE_CSPThe enforced policy, as a dict rather than a header string. Absent from the 5.2 LTS entirely, which is why there are three live answers for CSP in Django.
- accepts
<dict of directives>- default
{}- set in
settings.py- since
Django 6.0
SECURE_CSP_REPORT_ONLYThe same shape in report-only mode. This is where a policy should start — enforcing first is how CSP takes the front end down.
- accepts
<dict of directives>- default
{}- set in
settings.py- since
Django 6.0
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