skip to content

In Django 6.x, how do you enable the built-in Content Security Policy, and what do SECURE_CSP and SECURE_CSP_REPORT_ONLY control?

level: juniorimportance: must knowfreq 40%

answer

  1. new in 6.0
  2. one middleware, two settings
  3. dictionary of directives
  4. enforce versus report-only header
  5. django.utils.csp.CSP constants

basics

~10 s

Add django.middleware.csp.ContentSecurityPolicyMiddleware to MIDDLEWARE and fill SECURE_CSP (enforced header) and/or SECURE_CSP_REPORT_ONLY (report-only header) with directive dictionaries. Both default to {}, which sends no header.

solid answer

~30 s

Since Django 6.0, CSP is built in. I add `django.middleware.csp.ContentSecurityPolicyMiddleware` to `MIDDLEWARE`, since `startproject` does not, and describe the policy as a dict mapping each directive to a list of sources, preferably using `django.utils.csp.CSP` constants such as `CSP.SELF` and `CSP.NONE` so the quoting is right. `SECURE_CSP` becomes the enforcing `Content-Security-Policy` header; `SECURE_CSP_REPORT_ONLY` becomes `Content-Security-Policy-Report-Only`, which only reports violations. Both default to `{}`, and an empty dict means no header at all. The middleware builds the header on the way out, skips a header a view already set, and a system check (`security.E026`) rejects a setting that is not a dict.

code

python · 18 lines
python
from django.utils.csp import CSP

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
    "django.middleware.csp.ContentSecurityPolicyMiddleware",
]

SECURE_CSP = {
    "default-src": [CSP.SELF],
    "img-src": [CSP.SELF, "https:"],
    "object-src": [CSP.NONE],
}

go deeper

for a junior

Recall the middleware path, the two setting names, and that the settings are dictionaries of directive to list of sources, empty by default.

for a middle

Explain how the dict renders (lists, sets, True, None), why the CSP constants carry quoted keywords, and the difference between the enforcing and report-only headers.

for a senior

Show operational awareness: the middleware is opt-in, existing headers win, one layer should own the policy, and 5.2 LTS projects lack the feature entirely.

for a principal

Decide where the policy lives (Django settings versus the edge) and how it is reviewed, versioned and rolled out across services.

## What Django ships since 6.0 A **Content Security Policy (CSP)** is a response header that tells the browser which sources of scripts, styles, images, frames and other content a page may use. Before Django 6.0, projects set the header by hand or through a third-party package. Django 6.0 added built-in support: - `django.middleware.csp.ContentSecurityPolicyMiddleware`, which attaches the headers; - the settings `SECURE_CSP` and `SECURE_CSP_REPORT_ONLY`, which describe the policies; - the `django.utils.csp.CSP` enum of keyword constants; - per-view decorators and nonce support, covered separately. Django 5.2 LTS has none of this, which matters when a team on the LTS reads 6.x documentation. ## Turning it on 1. Add `"django.middleware.csp.ContentSecurityPolicyMiddleware"` to `MIDDLEWARE`. The settings template used by `startproject` does not include it. 2. Set `SECURE_CSP`, `SECURE_CSP_REPORT_ONLY`, or both, to a dictionary. 3. Deploy and inspect the response headers. With the middleware installed but both settings left at their default `{}`, nothing changes: an empty configuration adds no header. ## How a settings dictionary becomes a header The middleware calls `build_policy()` on each dictionary. Each key is a directive name; the value decides how it renders: | Value in the dict | Rendered as | |---|---| | a list or tuple, e.g. `[CSP.SELF, "https:"]` | `img-src 'self' https:` | | a set | the same, but sorted so the header is stable between requests | | a single string | treated as a one-item list | | `True` | the bare directive, e.g. `upgrade-insecure-requests` | | `None` or `False` | the directive is omitted | Directives are joined with `; `. A system check, `security.E026`, reports an error when either setting is something other than a dictionary. ```python from django.utils.csp import CSP SECURE_CSP = { "default-src": [CSP.SELF], "img-src": [CSP.SELF, "https:"], "object-src": [CSP.NONE], "upgrade-insecure-requests": True, } # Content-Security-Policy: default-src 'self'; img-src 'self' https:; # object-src 'none'; upgrade-insecure-requests ``` ## Enforced versus report-only | Setting | Header | Browser behaviour | |---|---|---| | `SECURE_CSP` | `Content-Security-Policy` | blocks violating content and can report it | | `SECURE_CSP_REPORT_ONLY` | `Content-Security-Policy-Report-Only` | allows everything, only reports violations | The two are independent. Use report-only alone to trial a new policy, enforce alone once it is verified, or both to keep an enforced baseline while trialling a stricter candidate. Reports are only sent if the policy itself contains a reporting directive such as `report-uri`; Django does not ship an endpoint to receive them. ## Why use the CSP constants CSP keywords must be written with **single quotes inside the value**: `'self'`, `'none'`, `'unsafe-inline'`. An unquoted `self` is read by the browser as a host name. The `CSP` enum (`CSP.SELF`, `CSP.NONE`, `CSP.UNSAFE_INLINE`, `CSP.STRICT_DYNAMIC`, `CSP.UNSAFE_EVAL`, `CSP.WASM_UNSAFE_EVAL`, `CSP.UNSAFE_HASHES`, `CSP.REPORT_SAMPLE`) carries the correctly quoted strings, so typos and quoting mistakes become Python errors instead of silently wrong policies. `CSP.NONCE` is different: it is a Django placeholder that the middleware replaces with a per-request nonce. ## Verifying the header in a test Because the policy is plain settings, it is easy to pin down in Django's test suite. `django.test.override_settings` can set `SECURE_CSP` for one test, and the test client exposes response headers: ```python from django.test import TestCase, override_settings from django.utils.csp import CSP @override_settings(SECURE_CSP={"default-src": [CSP.SELF]}) class CspHeaderTests(TestCase): def test_policy_is_sent(self): response = self.client.get("/") self.assertEqual(response.headers["Content-Security-Policy"], "default-src 'self'") ``` A test like this catches the two most common deployment mistakes: the middleware missing from `MIDDLEWARE` in one settings module, and a policy dictionary edited into a shape that renders differently from what reviewers expected. ## Behaviours worth knowing - **Existing headers win.** If a view or an inner layer already set `Content-Security-Policy`, the middleware leaves it alone. - **The policy is global by default.** Every response through the middleware gets the same policy unless a view overrides it with a decorator. - **CSP is defence in depth.** It limits the damage of an injection that got past template autoescaping; it does not replace escaping. - **Designing the directive values** (which sources, whether to use `'strict-dynamic'`) is a browser-security question; Django's job is to deliver whatever dictionary you give it, correctly formatted, on every response.

  • What happens if SECURE_CSP is written as a string copied from another project's header?
    Django's `security.E026` system check reports an error because the setting must be a dictionary. The middleware builds the header from directive-to-values pairs, so the fix is to translate the string into a dict, ideally with `CSP` constants for the quoted keywords.
  • A reverse proxy already adds a Content-Security-Policy header. Does Django's middleware add a second one?
    Django only checks the response it is building: if a view or inner layer already set the header, the middleware leaves it alone. A header added later by a proxy is outside Django's view, so the two can end up duplicated or conflicting; pick one layer to own the policy.

saying these in an interview costs you the question

  • Django has always had CSP built in; it is enabled by default.
  • Writing "self" without inner quotes in SECURE_CSP is the same as CSP.SELF.
  • SECURE_CSP_REPORT_ONLY blocks violations but also reports them.
  • An empty SECURE_CSP dict sends a default-src 'self' policy.
  • Django stores and displays CSP violation reports for you.