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?
answer
- new in 6.0
- one middleware, two settings
- dictionary of directives
- enforce versus report-only header
- django.utils.csp.CSP constants
basics
~10 sAdd 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 sSince 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 linesfrom 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
Recall the middleware path, the two setting names, and that the settings are dictionaries of directive to list of sources, empty by default.
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.
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.
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.