FastAPI & Django Security Middleware
Framework-level header injection provides application-layer defense against cross-site scripting, clickjacking, and protocol downgrade attacks. Implementing Server & Platform Implementation Guides standards ensures consistent header propagation across modern Python web stacks. This guide details production-ready middleware configurations, explicit verification workflows, and architectural trade-offs for securing Django and FastAPI deployments.
Threat Model & Header Baseline Requirements
Map OWASP Top 10 vulnerabilities to mandatory HTTP response headers before deploying middleware. Establish a strict baseline to mitigate injection, data leakage, and transport-layer attacks.
| OWASP Category | Required Header | Directive Baseline | Security Impact |
|---|---|---|---|
| A01:2021 Broken Access Control | Strict-Transport-Security |
max-age=31536000; includeSubDomains; preload |
Enforces TLS, prevents SSL stripping & MITM |
| A03:2021 Injection | Content-Security-Policy |
default-src 'self'; script-src 'self' 'nonce-{random}'; |
Blocks XSS, restricts resource origins |
| A05:2021 Security Misconfiguration | X-Content-Type-Options |
nosniff |
Prevents MIME-type sniffing & drive-by downloads |
| A09:2021 Security Logging & Monitoring | X-Frame-Options |
DENY or SAMEORIGIN |
Mitigates clickjacking & UI redress attacks |
| A04:2021 Insecure Design | Referrer-Policy |
strict-origin-when-cross-origin |
Controls referrer leakage on cross-origin requests |
| A07:2021 Identification & Authentication | Permissions-Policy |
geolocation=(), camera=(), microphone=() |
Restricts browser feature access at origin |
Header Precedence Hierarchy: Application > Reverse Proxy > CDN > Browser Cache. Always enforce headers at the application layer first, then strip duplicates at the proxy layer to prevent conflicting directives.
CSP Directive Scoping: API endpoints require default-src 'self' with explicit connect-src for WebSocket/GraphQL routes. SPAs require script-src with nonces or strict hashes. Never deploy unsafe-inline in production.
Django Security Middleware Configuration
Enable built-in security middleware via django.middleware.security.SecurityMiddleware. Configure SECURE_HSTS_SECONDS, SECURE_CONTENT_TYPE_NOSNIFF, and X_FRAME_OPTIONS. Evaluate framework defaults against organizational risk tolerance. Compare Django security middleware vs custom headers to determine when to extend MIDDLEWARE with custom classes.
# settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
# Place SecurityMiddleware FIRST to ensure headers attach before CORS/CSRF
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'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
X_FRAME_OPTIONS = 'DENY'
SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin'
Security Impact: SecurityMiddleware attaches headers synchronously during the response phase. SECURE_HSTS_PRELOAD enables submission to browser preload lists, eliminating first-visit downgrade windows. X_FRAME_OPTIONS overrides legacy clickjacking vectors.
Verification Steps:
- Run
python manage.py check --deployto validate production security settings. - Execute
curl -sI https://yourdomain.com | grep -iE '(strict-transport|x-content-type|x-frame|referrer-policy)' - Confirm
SECURE_SSL_REDIRECTdoes not trigger redirect loops behind load balancers (setSECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')if required).
FastAPI & Starlette Header Implementation
Implement middleware stack using Starlette’s Middleware class. Inject security headers via @app.middleware("http") decorator or custom BaseHTTPMiddleware. Handle async request/response lifecycle without blocking I/O. Ensure CORS and security headers do not conflict during OPTIONS preflight.
# main.py
from fastapi import FastAPI, Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
app = FastAPI()
@app.middleware("http")
async def add_security_headers(request: Request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains; preload"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Permissions-Policy"] = "geolocation=(), camera=(), microphone=()"
# CSP should be injected dynamically if using nonces
return response
Security Impact: The @app.middleware("http") decorator executes asynchronously, preserving FastAPI’s non-blocking architecture. Headers are applied after route resolution but before response serialization, ensuring consistent attachment across all endpoints including 4xx/5xx errors.
Verification Steps:
- Test async overhead:
ab -n 1000 -c 50 https://localhost:8000/api/healthand compare baseline vs middleware latency. - Validate preflight:
curl -I -X OPTIONS https://yourdomain.com/api/v1/resource -H "Origin: https://example.com" -H "Access-Control-Request-Method: POST" - Confirm headers persist on error responses:
curl -sI https://yourdomain.com/nonexistent | grep -iE '(x-content-type|strict-transport)'
Compatibility Trade-offs & Framework Limitations
Evaluate synchronous vs asynchronous middleware performance overhead. Address header duplication when deploying behind reverse proxies. Note Django’s synchronous request-response cycle constraints versus FastAPI’s async-native architecture. Review Apache .htaccess & VirtualHost Hardening directives to prevent server-level overrides from conflicting with application-layer injections.
| Factor | Django (WSGI/ASGI) | FastAPI (ASGI) | Mitigation Strategy |
|---|---|---|---|
| Execution Model | Synchronous by default; ASGI requires daphne/uvicorn |
Native async; await call_next() preserves event loop |
Use BaseHTTPMiddleware for async safety; avoid blocking I/O |
| Header Merging | Overwrites duplicates; SecurityMiddleware runs early |
Appends to MutableHeaders; later middleware can override |
Enforce single injection point; disable proxy-level add_header |
| Latency Impact | ~2-5ms per request (negligible) | ~1-3ms per request (async non-blocking) | Offload static headers to edge if sub-10ms SLA required |
| Error Responses | Headers attach to 4xx/5xx automatically | Requires explicit middleware wrapping or custom exception handlers | Implement @app.exception_handler to inject headers on failures |
Header Deduplication Strategy: When multiple layers inject identical headers, browsers apply the last received directive. Always configure the application layer as the source of truth, then use proxy directives to strip upstream duplicates before edge delivery.
Reverse Proxy & Server-Level Integration
Offload static security headers to the edge layer when application middleware introduces unacceptable latency. Configure proxy pass directives to strip or append headers safely. Reference Nginx Security Headers Configuration for upstream header management. Implement proxy_hide_header and add_header directives to maintain consistent security posture across the stack.
# nginx.conf
server {
listen 443 ssl;
server_name api.example.com;
# Strip application-layer duplicates to prevent conflicts
proxy_hide_header Strict-Transport-Security;
proxy_hide_header X-Content-Type-Options;
proxy_hide_header X-Frame-Options;
proxy_hide_header Referrer-Policy;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# Inject edge-level security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
}
Security Impact: Nginx’s always flag ensures headers attach to all response codes (including 4xx/5xx), bypassing application error-handling gaps. proxy_hide_header prevents duplicate header injection that triggers browser rejection or CSP parsing failures.
Verification Steps:
- Validate Nginx syntax:
nginx -t && systemctl reload nginx - Confirm edge injection:
curl -sI https://yourdomain.com | grep -iE '(strict-transport|x-content-type|x-frame)' - Verify no duplicates:
curl -sI https://yourdomain.com | grep -c "Strict-Transport-Security"(must return1)
Verification & Diagnostic Workflows
Execute automated header validation using curl -I, browser developer tools, and CI/CD pipeline scripts. Implement regex-based assertions for CSP directive completeness and HSTS preload eligibility. Validate CORS preflight responses do not leak security headers. Run periodic audits against securityheaders.com and Mozilla Observatory scoring.
Diagnostic Commands:
# 1. Extract baseline security headers
curl -sI https://yourdomain.com | grep -iE '(strict-transport|x-content-type|x-frame|referrer-policy|content-security-policy|permissions-policy)'
# 2. Validate HSTS preload eligibility
curl -sI https://yourdomain.com | grep -i "strict-transport-security" | grep -q "preload" && echo "PRELOAD READY" || echo "PRELOAD MISSING"
# 3. Check for duplicate headers
curl -sI https://yourdomain.com | awk '/^[Ss]trict-Transport-Security/ {count++} END {if(count>1) print "FAIL: Duplicate HSTS"; else print "PASS: Unique HSTS"}'
CI/CD Integration Script (Python):
import requests
import sys
def validate_headers(url: str):
required = {
"strict-transport-security": "max-age=31536000",
"x-content-type-options": "nosniff",
"x-frame-options": "DENY"
}
r = requests.head(url, allow_redirects=True, timeout=10)
headers = {k.lower(): v for k, v in r.headers.items()}
failures = []
for header, expected in required.items():
if header not in headers:
failures.append(f"MISSING: {header}")
elif expected not in headers[header]:
failures.append(f"INVALID: {header}={headers[header]}")
if failures:
print("HEADER AUDIT FAILED:")
for f in failures: print(f" - {f}")
sys.exit(1)
print("HEADER AUDIT PASSED")
if __name__ == "__main__":
validate_headers(sys.argv[1])
Troubleshooting Common Misconfigurations
Identify and resolve duplicate header injection from overlapping middleware layers. Fix CSP blocking legitimate API endpoints or third-party CDNs. Resolve mixed-content warnings when HSTS is active but internal assets use HTTP. Address browser caching of incorrect headers due to missing Vary directives. Implement fallback logging for header injection failures in production.
| Misconfiguration | Root Cause | Resolution Workflow |
|---|---|---|
Duplicate Strict-Transport-Security headers causing browser rejection |
App + Proxy both inject HSTS | Use proxy_hide_header in Nginx or Header unset in Apache. Enforce single source of truth. |
| Overly restrictive CSP blocking WebSocket upgrades or API calls | Missing connect-src or ws:/wss: schemes |
Add connect-src 'self' https://api.example.com wss://ws.example.com; to CSP. Test with report-uri before enforcing. |
Missing X-Content-Type-Options enabling MIME sniffing attacks |
Framework defaults disabled or proxy strips header | Explicitly set response.headers["X-Content-Type-Options"] = "nosniff". Verify with curl -sI. |
Incorrect Referrer-Policy stripping authentication tokens on redirects |
Policy set to no-referrer or origin |
Switch to strict-origin-when-cross-origin. Validate token persistence across OAuth flows. |
| FastAPI async middleware blocking synchronous Django fallback routes | Mixed WSGI/ASGI deployment or blocking call_next() |
Isolate sync routes via run_in_executor. Ensure middleware uses await exclusively. |
Cache Poisoning Prevention: Always attach Vary: Origin and Vary: Accept-Encoding when injecting security headers conditionally. This prevents reverse proxies from serving unsecured headers to subsequent requests.
Production Fallback Logging: Implement structured logging for header attachment failures:
import logging
logger = logging.getLogger("security.middleware")
@app.middleware("http")
async def secure_headers(request: Request, call_next):
try:
response = await call_next(request)
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
return response
except Exception as e:
logger.error("Security header injection failed", extra={"path": request.url.path, "error": str(e)})
raise