139 lines
5.2 KiB
Python
139 lines
5.2 KiB
Python
"""
|
|
Security headers middleware for DMARQ application.
|
|
|
|
Implements various security headers to protect against common web vulnerabilities:
|
|
- Content Security Policy (CSP)
|
|
- X-Frame-Options
|
|
- X-Content-Type-Options
|
|
- Strict-Transport-Security (HSTS)
|
|
- X-XSS-Protection
|
|
- Referrer-Policy
|
|
- Permissions-Policy
|
|
"""
|
|
|
|
import logging
|
|
from typing import Callable
|
|
|
|
from fastapi import Request
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
from starlette.responses import Response
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
|
"""
|
|
Middleware to add security headers to all HTTP responses.
|
|
"""
|
|
|
|
def __init__(self, app, environment: str = "development"):
|
|
"""
|
|
Initialize security headers middleware.
|
|
|
|
Args:
|
|
app: FastAPI application instance
|
|
environment: Application environment (development/production)
|
|
"""
|
|
super().__init__(app)
|
|
self.environment = environment
|
|
|
|
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
|
"""
|
|
Process the request and add security headers to the response.
|
|
|
|
Args:
|
|
request: Incoming HTTP request
|
|
call_next: Next middleware/handler in the chain
|
|
|
|
Returns:
|
|
HTTP response with security headers added
|
|
"""
|
|
response = await call_next(request)
|
|
|
|
# Content Security Policy (CSP)
|
|
# Restricts sources of content that can be loaded
|
|
#
|
|
# SECURITY TODO: Current CSP includes 'unsafe-inline' which weakens
|
|
# XSS protection. To remove it:
|
|
#
|
|
# For script-src 'unsafe-inline':
|
|
# 1. Move all inline <script> tags from templates to external .js files
|
|
# 2. OR implement CSP nonces for inline scripts (requires template changes)
|
|
# 3. Convert any inline event handlers (onclick, etc.) to addEventListener
|
|
#
|
|
# For style-src 'unsafe-inline':
|
|
# 1. Move inline styles to CSS files or use style tags with nonces
|
|
# 2. Replace style="" attributes with CSS classes
|
|
# 3. OR implement CSP nonces for inline styles
|
|
#
|
|
# Target secure CSP (no inline):
|
|
# "script-src 'self'"
|
|
# "style-src 'self' https://fonts.googleapis.com"
|
|
#
|
|
# See: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
|
|
csp_directives = [
|
|
"default-src 'self'",
|
|
# TODO: Remove 'unsafe-inline' - requires moving inline scripts to external files # pylint: disable=fixme
|
|
"script-src 'self' 'unsafe-inline'"
|
|
" https://cdn.tailwindcss.com https://cdn.jsdelivr.net",
|
|
# TODO: Remove 'unsafe-inline' - requires moving inline styles to CSS or using nonces # pylint: disable=fixme
|
|
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com"
|
|
" https://cdn.jsdelivr.net",
|
|
"font-src 'self' https://fonts.gstatic.com",
|
|
"img-src 'self' data: https:",
|
|
"connect-src 'self'",
|
|
"frame-ancestors 'none'", # Prevent framing
|
|
"base-uri 'self'",
|
|
"form-action 'self'",
|
|
]
|
|
response.headers["Content-Security-Policy"] = "; ".join(csp_directives)
|
|
|
|
# X-Frame-Options: Prevent clickjacking attacks
|
|
# 'DENY' prevents the page from being displayed in a frame
|
|
response.headers["X-Frame-Options"] = "DENY"
|
|
|
|
# X-Content-Type-Options: Prevent MIME type sniffing
|
|
# Forces browsers to respect the declared Content-Type
|
|
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
|
|
# X-XSS-Protection: Enable browser XSS protection
|
|
# Note: Modern browsers rely more on CSP, but this provides defense-in-depth
|
|
response.headers["X-XSS-Protection"] = "1; mode=block"
|
|
|
|
# Referrer-Policy: Control referrer information
|
|
# 'strict-origin-when-cross-origin' provides good balance of privacy and functionality
|
|
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
|
|
|
|
# Permissions-Policy: Control browser features
|
|
# Disable features that aren't needed
|
|
permissions_policies = [
|
|
"accelerometer=()",
|
|
"camera=()",
|
|
"geolocation=()",
|
|
"gyroscope=()",
|
|
"magnetometer=()",
|
|
"microphone=()",
|
|
"payment=()",
|
|
"usb=()",
|
|
]
|
|
response.headers["Permissions-Policy"] = ", ".join(permissions_policies)
|
|
|
|
# Strict-Transport-Security (HSTS): Force HTTPS
|
|
# Only enable in production with HTTPS
|
|
if self.environment == "production":
|
|
# max-age=31536000 = 1 year
|
|
# includeSubDomains applies to all subdomains
|
|
# preload allows inclusion in browser HSTS preload lists
|
|
response.headers["Strict-Transport-Security"] = (
|
|
"max-age=31536000; includeSubDomains; preload"
|
|
)
|
|
|
|
# Cache-Control for sensitive pages
|
|
# Prevent caching of potentially sensitive data
|
|
if request.url.path.startswith("/api/"):
|
|
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, private"
|
|
response.headers["Pragma"] = "no-cache"
|
|
response.headers["Expires"] = "0"
|
|
|
|
return response
|