Security headers and SecurityMiddleware
Most guides present this as a list of headers to turn on. Read from
global_settings.py, three of them are already on:
| Setting | Default | Action |
|---|---|---|
SECURE_CONTENT_TYPE_NOSNIFF |
True |
verify only |
SECURE_REFERRER_POLICY |
"same-origin" |
verify only |
SECURE_CROSS_ORIGIN_OPENER_POLICY |
"same-origin" |
verify only |
SECURE_HSTS_SECONDS |
0 |
set this |
SECURE_HSTS_INCLUDE_SUBDOMAINS |
False |
set with care |
SECURE_HSTS_PRELOAD |
False |
set with more care |
The corresponding checks confirm it. check_content_type_nosniff passes unless you
explicitly set the value to something other than True, and check_referrer_policy
fires W022 only when the setting is None. Neither warns on a stock project.
So the honest version of this page is: you are not adding three headers, you are confirming nobody removed them, and adding HSTS.
The one thing that makes all of them inert is the middleware being absent — that is
W001, and it is the first thing to check.
MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", # must be present, and early # ...]
SECURE_HSTS_SECONDS = 31536000 # 1 year — read the warning below firstSECURE_HSTS_INCLUDE_SUBDOMAINS = Truecurl -sI https://example.com/ | grep -iE 'strict-transport|x-content-type|referrer-policy|cross-origin-opener'You do not have 'django.middleware.security.SecurityMiddleware' in your MIDDLEWARE so the SECURE_HSTS_SECONDS, SECURE_CONTENT_TYPE_NOSNIFF, SECURE_REFERRER_POLICY, SECURE_CROSS_ORIGIN_OPENER_POLICY, and SECURE_SSL_REDIRECT settings will have no effect.The settings are being read and ignored. This is the finding to check first — without the middleware, every other setting on this page is inert.
Read in: django/core/checks/security/base.py — id security.W001You have not set a value for the SECURE_HSTS_SECONDS setting. If your entire site is served only over SSL, you may want to consider setting a value and enabling HTTP Strict Transport Security.SECURE_HSTS_SECONDS is 0, so no Strict-Transport-Security header is sent.
Read in: django/core/checks/security/base.py — id security.W004HSTS is the one control here that can take your domain offline
Section titled “HSTS is the one control here that can take your domain offline”Strict-Transport-Security tells a browser to refuse plain HTTP for this host for the
next n seconds, and the browser will not forget on request. You cannot undo a
one-year header by removing it; every browser that already saw it keeps enforcing it
until the clock runs out.
Ramp up rather than starting at a year:
SECURE_HSTS_SECONDS = 60 # then 3600, then 86400, then 31536000Add SECURE_HSTS_INCLUDE_SUBDOMAINS = True only once you are certain every
subdomain serves HTTPS — including ones you do not think about, like a legacy
docs. or an internal staging. host. The flag applies to all of them.
SECURE_HSTS_PRELOAD is a further step again: it opts into a list compiled into
browsers themselves, and removal from that list takes months. W021 will suggest it;
treat the suggestion as optional rather than a finding to clear.
Setting HSTS before HTTPS works everywhere is how a site becomes unreachable. The header is a commitment on behalf of every future visit, and the ramp exists so that a mistake expires in a minute instead of a year.
If Django sits behind a proxy that also sets these headers, you will end up with two sources of truth and possibly duplicated values. Decide which layer owns them — that question is on application vs server.
Related
Section titled “Related”- SSL redirect behind a proxy — HSTS is pointless until this works
- SECURE_BROWSER_XSS_FILTER was removed — the header this page does not set
- Application vs server — if a proxy also sets these headers, decide which layer owns them