In Django, what does SecurityMiddleware do, and which security headers does a new project send without changing any settings?
answer
- first in the startproject stack
- three headers on by default
- HTTPS features off by default
- a separate middleware for framing
basics
~10 sSecurityMiddleware 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.
solid answer
~40 s`django.middleware.security.SecurityMiddleware` is the first entry in the `MIDDLEWARE` that `startproject` generates. It reads the `SECURE_*` settings once, when the middleware is created, and does two jobs: redirect plain-HTTP requests to HTTPS when `SECURE_SSL_REDIRECT` is on, and add security headers to responses. Out of the box it sends `X-Content-Type-Options: nosniff`, `Referrer-Policy: same-origin` and `Cross-Origin-Opener-Policy: same-origin`. The HTTPS features are off: `SECURE_SSL_REDIRECT = False` and `SECURE_HSTS_SECONDS = 0`, because only the deployer knows the site is HTTPS-only. Framing protection comes from a different class, `XFrameOptionsMiddleware`, which sends `X-Frame-Options: DENY` by default. All of these leave a header alone if a view already set it.
code
python · 7 lines# settings.py for an HTTPS-only production site (defaults shown as comments)
SECURE_SSL_REDIRECT = True # default False
SECURE_HSTS_SECONDS = 3600 # default 0: start small
SECURE_CONTENT_TYPE_NOSNIFF = True # default True
SECURE_REFERRER_POLICY = "same-origin" # default "same-origin"
SECURE_CROSS_ORIGIN_OPENER_POLICY = "same-origin" # default "same-origin"
X_FRAME_OPTIONS = "DENY" # default "DENY"go deeper
Recall that SecurityMiddleware comes first in the default stack, sends nosniff, Referrer-Policy and COOP by default, and that X-Frame-Options comes from XFrameOptionsMiddleware.
Explain why the redirect and HSTS are opt-in, the set-if-absent behaviour, and what each default header protects against.
Know which settings a production HTTPS deployment must add and which removed settings in old guides to ignore.
Decide which headers Django owns and which the edge owns, so the policy is set once and tested.
## Two middleware classes, one purpose Django's response-header hardening is split between two classes that `startproject` puts into `MIDDLEWARE`: - `django.middleware.security.SecurityMiddleware`, listed first, driven by the `SECURE_*` settings; - `django.middleware.clickjacking.XFrameOptionsMiddleware`, listed last, driven by `X_FRAME_OPTIONS`. `SecurityMiddleware` reads its settings once, in its constructor, so changing them requires a restart, as with any settings change in production. ## What SecurityMiddleware does It has two phases: 1. **On the request**, if `SECURE_SSL_REDIRECT` is `True` and `request.is_secure()` is `False`, it returns a permanent (301) redirect to the `https://` version of the URL, unless the path matches `SECURE_REDIRECT_EXEMPT`. 2. **On the response**, it adds headers according to the settings, each only if the response does not already carry it. ## Defaults in a new project | Setting | Default | Effect | |---|---|---| | `SECURE_CONTENT_TYPE_NOSNIFF` | `True` | `X-Content-Type-Options: nosniff` | | `SECURE_REFERRER_POLICY` | `'same-origin'` | `Referrer-Policy: same-origin` | | `SECURE_CROSS_ORIGIN_OPENER_POLICY` | `'same-origin'` | `Cross-Origin-Opener-Policy: same-origin` | | `SECURE_SSL_REDIRECT` | `False` | no HTTP to HTTPS redirect | | `SECURE_HSTS_SECONDS` | `0` | no `Strict-Transport-Security` header | | `SECURE_PROXY_SSL_HEADER` | `None` | `is_secure()` trusts only the connection Django itself sees | | `X_FRAME_OPTIONS` | `'DENY'` | `X-Frame-Options: DENY` (from `XFrameOptionsMiddleware`) | The split is deliberate. The three always-on headers are safe for any site. The HTTPS features are not: redirecting or sending HSTS on a site that is not fully HTTPS can lock users out, so Django leaves them to the deployer. ## What each default header does - **`nosniff`** stops browsers guessing a response's content type from its bytes, so an uploaded text file cannot be sniffed and run as a script. - **`Referrer-Policy: same-origin`** sends the full referrer only on same-origin requests and none to other sites, which keeps URLs with tokens or IDs from leaking to third parties. It can be set to any valid policy, or to a comma-separated string or list for fallbacks; `security.E023` rejects invalid values. - **`Cross-Origin-Opener-Policy: same-origin`** puts the page in its own browsing-context group, so a cross-origin page that opened it (or that it opens) cannot keep a reference to its window. Valid values are `same-origin`, `same-origin-allow-popups` and `unsafe-none`; `security.E024` rejects others. - **`X-Frame-Options: DENY`** tells browsers not to render the page in any frame, the classic clickjacking defence. Setting `SECURE_REFERRER_POLICY` or `SECURE_CROSS_ORIGIN_OPENER_POLICY` to `None` turns that header off. ## Views still have the last word The response headers are added with set-if-absent semantics: - a view that sets its own `Referrer-Policy`, `X-Content-Type-Options` or `Cross-Origin-Opener-Policy` keeps it; - `Strict-Transport-Security` is added only when absent (and only on HTTPS requests); - `XFrameOptionsMiddleware` skips responses that already carry `X-Frame-Options` or were marked by `@xframe_options_exempt`. This makes per-view exceptions possible without disabling the middleware. ## Checking the headers in a test The defaults are easy to pin down with Django's test client, which runs the full middleware stack: ```python from django.test import TestCase class SecurityHeaderTests(TestCase): def test_default_headers(self): response = self.client.get("/") self.assertEqual(response.headers["X-Content-Type-Options"], "nosniff") self.assertEqual(response.headers["Referrer-Policy"], "same-origin") self.assertEqual(response.headers["X-Frame-Options"], "DENY") self.assertNotIn("Strict-Transport-Security", response.headers) ``` A test like this fails loudly if someone removes a middleware class from `MIDDLEWARE` or sets one of the settings to `None` while tidying configuration. ## What a deployer still has to do For an HTTPS-only production site, the defaults are a starting point. The deployer typically: 1. turns on `SECURE_SSL_REDIRECT` (or redirects at the proxy); 2. sets `SECURE_PROXY_SSL_HEADER` if TLS ends at a proxy that sets a trusted header; 3. rolls out HSTS with `SECURE_HSTS_SECONDS`, starting small; 4. marks cookies secure through the session and CSRF settings. The removed `SECURE_BROWSER_XSS_FILTER` setting is sometimes still quoted in old guides; it was dropped in Django 4.0 along with the obsolete header it controlled.
- Why doesn't Django enable SECURE_SSL_REDIRECT and HSTS by default?Both assume the whole site is reachable over HTTPS. On a development server, or behind a proxy Django cannot see through, the redirect loops or points at an HTTPS endpoint that does not exist, and HSTS makes browsers refuse plain HTTP for its whole max-age. Only the deployer can know it is safe, so they are opt-in.
- Can a single view send a different Referrer-Policy from the site default?Yes. `SecurityMiddleware` uses set-if-absent for `Referrer-Policy`, so a view that sets `response.headers["Referrer-Policy"]` itself keeps its value. The same holds for `X-Content-Type-Options` and `Cross-Origin-Opener-Policy`.
saying these in an interview costs you the question
- SecurityMiddleware redirects to HTTPS and sends HSTS by default.
- SecurityMiddleware is what sets X-Frame-Options.
- SecurityMiddleware overwrites any security header a view has set.
- SECURE_BROWSER_XSS_FILTER is still a current Django setting.
- Changing SECURE_* settings at runtime takes effect on the next request.