Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
203 lines
6.8 KiB
Python
203 lines
6.8 KiB
Python
"""
|
|
Security Headers Middleware
|
|
|
|
Adds security headers to all HTTP responses to protect against common attacks.
|
|
|
|
Headers added:
|
|
- X-Content-Type-Options: nosniff
|
|
- X-Frame-Options: DENY
|
|
- X-XSS-Protection: 1; mode=block
|
|
- Strict-Transport-Security (HSTS)
|
|
- Content-Security-Policy
|
|
- Referrer-Policy
|
|
- Permissions-Policy
|
|
|
|
Usage:
|
|
from middleware import SecurityHeadersMiddleware
|
|
|
|
app.add_middleware(SecurityHeadersMiddleware)
|
|
|
|
# Or with custom configuration:
|
|
app.add_middleware(
|
|
SecurityHeadersMiddleware,
|
|
hsts_enabled=True,
|
|
csp_policy="default-src 'self'",
|
|
)
|
|
"""
|
|
|
|
import os
|
|
from dataclasses import dataclass, field
|
|
from typing import Dict, List, Optional
|
|
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
from starlette.requests import Request
|
|
from starlette.responses import Response
|
|
|
|
|
|
@dataclass
|
|
class SecurityHeadersConfig:
|
|
"""Configuration for security headers."""
|
|
|
|
# X-Content-Type-Options
|
|
content_type_options: str = "nosniff"
|
|
|
|
# X-Frame-Options
|
|
frame_options: str = "DENY"
|
|
|
|
# X-XSS-Protection (legacy, but still useful for older browsers)
|
|
xss_protection: str = "1; mode=block"
|
|
|
|
# Strict-Transport-Security
|
|
hsts_enabled: bool = True
|
|
hsts_max_age: int = 31536000 # 1 year
|
|
hsts_include_subdomains: bool = True
|
|
hsts_preload: bool = False
|
|
|
|
# Content-Security-Policy
|
|
csp_enabled: bool = True
|
|
csp_policy: str = "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https:; frame-ancestors 'none'"
|
|
|
|
# Referrer-Policy
|
|
referrer_policy: str = "strict-origin-when-cross-origin"
|
|
|
|
# Permissions-Policy (formerly Feature-Policy)
|
|
permissions_policy: str = "geolocation=(), microphone=(), camera=()"
|
|
|
|
# Cross-Origin headers
|
|
cross_origin_opener_policy: str = "same-origin"
|
|
cross_origin_embedder_policy: str = "require-corp"
|
|
cross_origin_resource_policy: str = "same-origin"
|
|
|
|
# Development mode (relaxes some restrictions)
|
|
development_mode: bool = False
|
|
|
|
# Excluded paths (e.g., for health checks)
|
|
excluded_paths: List[str] = field(default_factory=lambda: ["/health", "/metrics"])
|
|
|
|
|
|
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
|
"""
|
|
Middleware that adds security headers to all responses.
|
|
|
|
Attributes:
|
|
config: SecurityHeadersConfig instance
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
app,
|
|
config: Optional[SecurityHeadersConfig] = None,
|
|
# Individual overrides for convenience
|
|
hsts_enabled: Optional[bool] = None,
|
|
csp_policy: Optional[str] = None,
|
|
csp_enabled: Optional[bool] = None,
|
|
development_mode: Optional[bool] = None,
|
|
):
|
|
super().__init__(app)
|
|
|
|
# Use provided config or create default
|
|
self.config = config or SecurityHeadersConfig()
|
|
|
|
# Apply individual overrides
|
|
if hsts_enabled is not None:
|
|
self.config.hsts_enabled = hsts_enabled
|
|
if csp_policy is not None:
|
|
self.config.csp_policy = csp_policy
|
|
if csp_enabled is not None:
|
|
self.config.csp_enabled = csp_enabled
|
|
if development_mode is not None:
|
|
self.config.development_mode = development_mode
|
|
|
|
# Auto-detect development mode from environment
|
|
if development_mode is None:
|
|
env = os.getenv("ENVIRONMENT", "development")
|
|
self.config.development_mode = env.lower() in ("development", "dev", "local")
|
|
|
|
def _build_hsts_header(self) -> str:
|
|
"""Build the Strict-Transport-Security header value."""
|
|
parts = [f"max-age={self.config.hsts_max_age}"]
|
|
if self.config.hsts_include_subdomains:
|
|
parts.append("includeSubDomains")
|
|
if self.config.hsts_preload:
|
|
parts.append("preload")
|
|
return "; ".join(parts)
|
|
|
|
def _get_headers(self) -> Dict[str, str]:
|
|
"""Build the security headers dictionary."""
|
|
headers = {}
|
|
|
|
# Always add these headers
|
|
headers["X-Content-Type-Options"] = self.config.content_type_options
|
|
headers["X-Frame-Options"] = self.config.frame_options
|
|
headers["X-XSS-Protection"] = self.config.xss_protection
|
|
headers["Referrer-Policy"] = self.config.referrer_policy
|
|
|
|
# HSTS (only in production or if explicitly enabled)
|
|
if self.config.hsts_enabled and not self.config.development_mode:
|
|
headers["Strict-Transport-Security"] = self._build_hsts_header()
|
|
|
|
# Content-Security-Policy
|
|
if self.config.csp_enabled:
|
|
headers["Content-Security-Policy"] = self.config.csp_policy
|
|
|
|
# Permissions-Policy
|
|
if self.config.permissions_policy:
|
|
headers["Permissions-Policy"] = self.config.permissions_policy
|
|
|
|
# Cross-Origin headers (relaxed in development)
|
|
if not self.config.development_mode:
|
|
headers["Cross-Origin-Opener-Policy"] = self.config.cross_origin_opener_policy
|
|
# Note: COEP can break loading of external resources, be careful
|
|
# headers["Cross-Origin-Embedder-Policy"] = self.config.cross_origin_embedder_policy
|
|
headers["Cross-Origin-Resource-Policy"] = self.config.cross_origin_resource_policy
|
|
|
|
return headers
|
|
|
|
async def dispatch(self, request: Request, call_next) -> Response:
|
|
# Skip security headers for excluded paths
|
|
if request.url.path in self.config.excluded_paths:
|
|
return await call_next(request)
|
|
|
|
# Process request
|
|
response = await call_next(request)
|
|
|
|
# Add security headers
|
|
for header_name, header_value in self._get_headers().items():
|
|
response.headers[header_name] = header_value
|
|
|
|
return response
|
|
|
|
|
|
def get_default_csp_for_environment(environment: str) -> str:
|
|
"""
|
|
Get a sensible default CSP for the given environment.
|
|
|
|
Args:
|
|
environment: "development", "staging", or "production"
|
|
|
|
Returns:
|
|
CSP policy string
|
|
"""
|
|
if environment.lower() in ("development", "dev", "local"):
|
|
# Relaxed CSP for development
|
|
return (
|
|
"default-src 'self' localhost:* ws://localhost:*; "
|
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; "
|
|
"style-src 'self' 'unsafe-inline'; "
|
|
"img-src 'self' data: https: blob:; "
|
|
"font-src 'self' data:; "
|
|
"connect-src 'self' localhost:* ws://localhost:* https:; "
|
|
"frame-ancestors 'self'"
|
|
)
|
|
else:
|
|
# Strict CSP for production
|
|
return (
|
|
"default-src 'self'; "
|
|
"script-src 'self' 'unsafe-inline'; "
|
|
"style-src 'self' 'unsafe-inline'; "
|
|
"img-src 'self' data: https:; "
|
|
"font-src 'self' data:; "
|
|
"connect-src 'self' https://breakpilot.app https://*.breakpilot.app; "
|
|
"frame-ancestors 'none'"
|
|
)
|