Skip to content

Security headers and SecurityMiddleware

Severity: mediumApplies to: Django 4.0+Applies to: Django 5.2 LTSApplies to: Django 6.0Facts last verified 2026-07-26 against Django 6.0 (main)

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.

the fix
settings.py
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", # must be present, and early
# ...
]
SECURE_HSTS_SECONDS = 31536000 # 1 year — read the warning below first
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
verify it workedRun this in: http response
Terminal window
curl -sI https://example.com/ | grep -iE 'strict-transport|x-content-type|referrer-policy|cross-origin-opener'
if you see this
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.W001
You 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.W004

HSTS 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 31536000

Add 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.

before you ship this

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.