skip to content

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

level: juniorimportance: should knowfreq 45%

answer

  1. first in the startproject stack
  2. three headers on by default
  3. HTTPS features off by default
  4. a separate middleware for framing

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.

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
python
# 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

for a junior

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.

for a middle

Explain why the redirect and HSTS are opt-in, the set-if-absent behaviour, and what each default header protects against.

for a senior

Know which settings a production HTTPS deployment must add and which removed settings in old guides to ignore.

for a principal

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.