skip to content

Built-In Protections

The defences Django ships and the settings that switch them on: CSRF middleware, CSP, SecurityMiddleware headers, ALLOWED_HOSTS and SECRET_KEY. Interviewers check you know which are on by default.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

22

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.
open as a page

In a Django template, what does {% csrf_token %} add to a POST form, and why does the submit fail with 403 without it?

level: juniorimportance: must knowfreq 80%

basics

~20 s

{% csrf_token %} renders a hidden csrfmiddlewaretoken input carrying a masked copy of the visitor's CSRF secret. CsrfViewMiddleware answers 403 Forbidden to a POST whose token is missing or does not match the csrftoken cookie.

open as a page

In Django, what does the ALLOWED_HOSTS setting do, and why does a site start answering 400 Bad Request right after DEBUG is set to False?

level: juniorimportance: must knowfreq 66%

basics

~20 s

ALLOWED_HOSTS lists the domain names a Django site may serve; request.get_host() rejects any other Host with DisallowedHost, a 400. An empty list is only tolerated for localhost while DEBUG=True, so switching DEBUG off without filling it breaks every request.

open as a page

A Django page's fetch() POST returns 403 'CSRF token missing'; how do you send the token correctly instead of exempting the view?

level: middleimportance: must knowfreq 72%

basics

~20 s

Read the token from the csrftoken cookie, or from a rendered csrfmiddlewaretoken input, and send it in an X-CSRFToken request header. Django reads the token only from form-encoded POST data or that header, never from a JSON body.

open as a page

A Django site behind a TLS-terminating load balancer enables SECURE_SSL_REDIRECT and every page now loops with redirects; why, and how do you fix it?

level: middleimportance: must knowfreq 60%

basics

~10 s

The balancer talks plain HTTP to Django, so request.is_secure() is False and SecurityMiddleware redirects HTTPS users again forever. Set SECURE_PROXY_SSL_HEADER to the header the balancer sets, or redirect at the balancer instead.

open as a page

In Django, what does SECRET_KEY actually sign, and what happens to your users when you change it without a fallback?

level: middleimportance: must knowfreq 55%

basics

~20 s

Django's SECRET_KEY keys the HMAC signatures on session data, the session auth hash, cookie-stored messages, password-reset tokens and django.core.signing output. Changing it without a fallback logs everyone out and voids those tokens; password hashes are unaffected.

open as a page

In Django, what does SecurityMiddleware do, and which security headers does a new project send without changing any settings?

level: juniorimportance: should knowfreq 45%

basics

~10 s

SecurityMiddleware applies the SECURE_* settings: HTTPS redirects, HSTS, nosniff, Referrer-Policy and COOP. By default it sends only nosniff, Referrer-Policy: same-origin and COOP: same-origin; XFrameOptionsMiddleware adds X-Frame-Options: DENY.

open as a page

How should a Django project load SECRET_KEY in production, and how do you generate a strong new value for it?

level: juniorimportance: should knowfreq 45%

basics

~10 s

Read SECRET_KEY from the environment or a secrets file with no hard-coded default, so a missing value fails loudly. Generate one with django.core.management.utils.get_random_secret_key(), which returns 50 random characters; never ship startproject's 'django-insecure-' key.

open as a page

In Django 6.1, how do you let a marketing page's inline script run under a nonce-based Content Security Policy without 'unsafe-inline'?

level: middleimportance: should knowfreq 35%

basics

~10 s

Put CSP.NONCE in script-src, add django.template.context_processors.csp to TEMPLATES, and render <script nonce="{{ csp_nonce }}">. The middleware swaps the placeholder for the per-request nonce in the header.

open as a page

In Django 6.x, how do you trial a stricter Content Security Policy in report-only mode first, and what does Django leave for you to build?

level: middleimportance: should knowfreq 30%

basics

~10 s

Put the candidate policy, including a report-uri directive, in SECURE_CSP_REPORT_ONLY while SECURE_CSP keeps the current one. Django sends both headers but has no endpoint for violation reports; you write that view.

open as a page

Django answers a POST from an SPA on https://app.example.com to https://api.example.com with 'Origin checking failed'; what is checked, and what fixes it?

level: middleimportance: should knowfreq 55%

basics

~10 s

Since Django 4.0, CsrfViewMiddleware accepts an unsafe request's Origin only if it equals the site's own scheme and host or matches CSRF_TRUSTED_ORIGINS. Add "https://app.example.com" there; the X-CSRFToken token check still runs afterwards.

open as a page

In Django, where exactly is the Host header checked against ALLOWED_HOSTS, and why is reading request.META['HTTP_HOST'] directly a security bug?

level: middleimportance: should knowfreq 34%

basics

~10 s

Django validates the host only inside HttpRequest.get_host(); no middleware checks it up front. Code that reads request.META['HTTP_HOST'] or request.headers['Host'] gets the raw, attacker-chosen value and bypasses ALLOWED_HOSTS entirely.

open as a page

In a custom Django view, how do you safely redirect to a user-supplied ?next= URL, and what does url_has_allowed_host_and_scheme() reject?

level: middleimportance: should knowfreq 40%

basics

~10 s

Check the value with django.utils.http.url_has_allowed_host_and_scheme(url, allowed_hosts={request.get_host()}, require_https=request.is_secure()) and fall back to a fixed URL when it returns False. It rejects foreign hosts, non-HTTP schemes and browser-parsing tricks.

open as a page

In Django, how does XFrameOptionsMiddleware protect against clickjacking, and how do you let one view be framed by your own pages?

level: middleimportance: should knowfreq 40%

basics

~10 s

XFrameOptionsMiddleware adds X-Frame-Options with the X_FRAME_OPTIONS value, DENY by default, so browsers refuse to frame pages. Decorate one view with @xframe_options_sameorigin to allow same-origin framing, or @xframe_options_exempt to skip the header.

open as a page

A Django 6.x marketing page uses a CSP nonce and is wrapped in cache_page; after the first visit its inline scripts are blocked. Why, and how do you fix it?

level: seniorimportance: should knowfreq 22%

basics

~20 s

On a cache_page hit the template is not rendered, so the lazy nonce is never read and the middleware drops it from the header, while the cached HTML carries the first visitor's nonce. Don't full-page cache nonce pages.

open as a page

When is Django's csrf_exempt safe on a view, such as an inbound webhook, and what must replace the protection it removes?

level: seniorimportance: should knowfreq 45%

basics

~20 s

csrf_exempt is safe only on a view that grants nothing on the strength of browser cookies, such as a server-to-server webhook. Replace the lost check with the caller's own authentication, typically a signature over the raw body, verified before acting.

open as a page

In Django, how do you roll out HSTS with SECURE_HSTS_SECONDS, SECURE_HSTS_INCLUDE_SUBDOMAINS and SECURE_HSTS_PRELOAD without locking users out?

level: seniorimportance: should knowfreq 40%

basics

~10 s

Set SECURE_HSTS_SECONDS to a small value such as 3600, confirm every page works over HTTPS, then raise it to about a year. Add includeSubDomains only when every subdomain is HTTPS, and preload last.

open as a page

In Django, why is setting SECURE_PROXY_SSL_HEADER dangerous when the proxy passes client-supplied X-Forwarded-Proto headers through, and what does a spoofed value change?

level: seniorimportance: should knowfreq 30%

basics

~10 s

SECURE_PROXY_SSL_HEADER makes request.is_secure() trust the named header. If clients can supply it, a plain-HTTP request claiming https is treated as secure, skipping the HTTPS redirect and misleading every is_secure()-based check.

open as a page

In Django 6.x, one marketing view embeds a video player that the site-wide SECURE_CSP blocks; how do csp_override and csp_report_only_override behave, and what traps come with them?

level: seniorimportance: nice to knowfreq 18%

basics

~10 s

csp_override(config) makes the middleware use config instead of SECURE_CSP for that view's responses; csp_report_only_override does the same for the report-only header. Overrides replace the whole policy, and {} removes the header.

open as a page

A security audit asks for Django's CSRF_COOKIE_HTTPONLY and CSRF_USE_SESSIONS set to True; what does each change, and what breaks?

level: seniorimportance: nice to knowfreq 25%

basics

~20 s

CSRF_COOKIE_HTTPONLY hides the csrftoken cookie from JavaScript, so AJAX code must read the token from a rendered {% csrf_token %} input. CSRF_USE_SESSIONS drops the cookie and stores the secret in the session, requiring SessionMiddleware before CsrfViewMiddleware.

open as a page

Your Django SECRET_KEY was committed to a public repository; how do you rotate it with SECRET_KEY_FALLBACKS without logging everyone out, and what does the fallback cost you?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

Set a freshly generated SECRET_KEY and move the leaked key into SECRET_KEY_FALLBACKS briefly, so active sessions verify and get re-signed, then remove it. While it stays there, anything the attacker signs with it also verifies.

open as a page