Django SecurityMiddleware vs Custom Headers: When to Use Each
This guide is part of the FastAPI & Django Security Middleware reference. Django ships exactly one header middleware, django.middleware.security.SecurityMiddleware, which sets a fixed subset of response headers from SECURE_* settings. Everything outside that subset — most importantly Content-Security-Policy and Permissions-Policy — requires a custom middleware class or a third-party package. The decision is not “built-in or custom”; it is “built-in plus custom for the gaps.” This page gives the exact settings, the gap list, and the ordering rules that decide which value wins.
Configuration Syntax & Exact Values
SecurityMiddleware reads these settings and emits the corresponding headers. The right column is the production baseline:
| Setting | Header emitted | Baseline value |
|---|---|---|
SECURE_HSTS_SECONDS |
Strict-Transport-Security (HTTPS only) |
31536000 |
SECURE_HSTS_INCLUDE_SUBDOMAINS |
adds includeSubDomains |
True |
SECURE_HSTS_PRELOAD |
adds preload |
True |
SECURE_CONTENT_TYPE_NOSNIFF |
X-Content-Type-Options: nosniff |
True |
SECURE_REFERRER_POLICY |
Referrer-Policy |
strict-origin-when-cross-origin |
SECURE_SSL_REDIRECT |
301 redirect http→https (not a header) | True |
X-Frame-Options is not part of SecurityMiddleware — it is emitted by the separate django.middleware.clickjacking.XFrameOptionsMiddleware from the X_FRAME_OPTIONS setting (DENY or SAMEORIGIN).
A custom middleware class fills the gaps. The minimal, override-safe form:
# myapp/middleware.py
class ExtraSecurityHeadersMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
# Headers SecurityMiddleware never sets:
response.setdefault("Permissions-Policy", "geolocation=(), camera=(), microphone=()")
response.setdefault("Cross-Origin-Opener-Policy", "same-origin")
return response
setdefault only writes the header if a view (or django-csp) has not already set it, so per-route overrides survive. Use plain assignment (response["..."] = ...) only when this middleware must be the authoritative source.
Server-Side Configuration
Django settings
# settings.py
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", # FIRST — outermost
"myapp.middleware.ExtraSecurityHeadersMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_SSL_REDIRECT = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"
X_FRAME_OPTIONS = "DENY"
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # behind TLS-terminating proxy
Django custom middleware
Place the custom class above SecurityMiddleware only when it must override a value SecurityMiddleware also sets. Django runs MIDDLEWARE top-to-bottom on the request and bottom-to-top on the response, so a class listed earlier runs later on the response and its value wins:
MIDDLEWARE = [
"myapp.middleware.ExtraSecurityHeadersMiddleware", # runs last on response → overrides
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
]
For headers SecurityMiddleware does not touch (CSP, Permissions-Policy), position relative to SecurityMiddleware is irrelevant — there is no conflict to resolve. Keep the custom class near the top for predictability.
FastAPI contrast. FastAPI/Starlette ships no SecurityMiddleware equivalent, so there are no built-ins to coordinate with — you set every header in one @app.middleware("http") function or ASGI class, and the last-registered middleware runs outermost. The full FastAPI pattern is in FastAPI & Django Security Middleware.
Diagnostic & Verification Steps
Inspect the raw headers. Expected output with the configuration above:
$ curl -sI https://your-domain.com/ | grep -iE '(strict-transport|x-frame|x-content-type|referrer-policy|permissions-policy)'
strict-transport-security: max-age=31536000; includeSubDomains; preload
x-frame-options: DENY
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
permissions-policy: geolocation=(), camera=(), microphone=()
Run the deploy check to audit the SECURE_* settings:
$ python manage.py check --deploy
System check identified no issues (0 silenced).
The deploy check warns about SECURE_HSTS_SECONDS = 0, SECURE_SSL_REDIRECT = False, and DEBUG = True. It does not check for a missing Content-Security-Policy or Permissions-Policy, because no setting controls them — a clean check is not proof those headers exist.
Edge Cases, Security Implications & Safe Rollback
Middleware order overwrites your value. If the custom class is listed after SecurityMiddleware, it runs earlier on the response and SecurityMiddleware overwrites any header they share (e.g. Referrer-Policy). Move the custom class above SecurityMiddleware to win the conflict, or simply use distinct headers so no conflict exists.
What SecurityMiddleware does NOT set. It sets only HSTS, X-Content-Type-Options, and Referrer-Policy (plus the SSL redirect). It does not set Content-Security-Policy, Permissions-Policy, Cross-Origin-Opener-Policy/Cross-Origin-Embedder-Policy, or X-Frame-Options (that last one is XFrameOptionsMiddleware). Shipping only the built-ins leaves the application with no XSS-mitigating CSP. Add django-csp or a custom class for these; the CSP-specific path is in setting CSP in Django middleware.
Proxy override and duplicates. A reverse proxy or CDN that also injects HSTS produces duplicate headers. Decide one owner: either strip the app’s value at the proxy (proxy_hide_header/Header unset) and assert at the edge, or disable the proxy’s injection and keep Django authoritative. Confirm uniqueness with curl -sI https://... | grep -c -i strict-transport-security returning 1. Also ensure the proxy forwards X-Forwarded-Proto, or Django sees plain HTTP and emits no HSTS at all.
Safe rollback. Removing a custom header class is non-destructive but can silently drop coverage:
- Comment out the custom middleware string in
MIDDLEWARE. - Run
python manage.py checkto catch config drift. - Reload workers gracefully:
systemctl reload gunicornorkill -HUP <pid>. - Re-run
curl -Iand confirm the built-in headers (X-Frame-Options, HSTS) are still present — rolling back the custom class must not leave clickjacking or feature-policy gaps.
Frequently Asked Questions
Does SecurityMiddleware set X-Frame-Options?
No. X-Frame-Options comes from django.middleware.clickjacking.XFrameOptionsMiddleware driven by the X_FRAME_OPTIONS setting. SecurityMiddleware sets HSTS, X-Content-Type-Options, Referrer-Policy, and the SSL redirect only.
Do I need a custom middleware if I use the built-ins?
Yes, if you want a Content-Security-Policy, Permissions-Policy, or cross-origin isolation headers — SecurityMiddleware sets none of those. Use django-csp for CSP and a small custom class for the rest.
Why does manage.py check --deploy pass when I have no CSP?
Because there is no Django setting for CSP, the deploy check has nothing to inspect. A clean deploy check audits only the SECURE_* settings; verify CSP and Permissions-Policy directly with curl -sI.
How do I make a custom header override a built-in one?
List the custom middleware above SecurityMiddleware in MIDDLEWARE. Django runs the response phase bottom-to-top, so the earlier-listed middleware touches the response last and its value wins.
Conclusion
Lean on the built-ins for the headers they cover — HSTS, X-Content-Type-Options, Referrer-Policy, and (via XFrameOptionsMiddleware) X-Frame-Options — and add one small custom class plus django-csp for the gaps. Roll it out incrementally: enable the SECURE_* settings in staging with a short SECURE_HSTS_SECONDS (such as 300), confirm with curl -I and manage.py check --deploy, then raise max-age to a year and add preload only once every subdomain is HTTPS-only.
Related
- FastAPI & Django Security Middleware
- Setting CSP in Django middleware
- Adding security headers in FastAPI middleware