"""Security response headers middleware. Adds standard security headers to all API responses: - X-Content-Type-Options: nosniff (prevent MIME sniffing) - X-Frame-Options: DENY (prevent clickjacking) - Referrer-Policy: strict-origin-when-cross-origin - Cache-Control: no-store (prevent browser caching of API responses) - Strict-Transport-Security (HSTS, production only -- breaks plain HTTP dev) - Content-Security-Policy (strict in production, relaxed for dev HMR) CSP directives: - script-src 'self' (production) blocks inline scripts -- XSS mitigation - style-src 'unsafe-inline' required for Tailwind, Framer Motion, Radix, Sonner - connect-src includes wss:/ws: for SSE and WebSocket connections - Dev mode adds 'unsafe-inline' and 'unsafe-eval' for Vite HMR """ from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request from starlette.responses import Response # Production CSP: strict -- no inline scripts allowed _CSP_PRODUCTION = "; ".join( [ "default-src 'self'", "script-src 'self'", "style-src 'self' 'unsafe-inline'", "img-src 'self' data: blob:", "font-src 'self'", "connect-src 'self' wss: ws:", "worker-src 'self'", "frame-ancestors 'none'", "base-uri 'self'", "form-action 'self'", ] ) # Development CSP: relaxed for Vite HMR (hot module replacement) _CSP_DEV = "; ".join( [ "default-src 'self'", "script-src 'self' 'unsafe-inline' 'unsafe-eval'", "style-src 'self' 'unsafe-inline'", "img-src 'self' data: blob:", "font-src 'self'", "connect-src 'self' http://localhost:* ws://localhost:* wss:", "worker-src 'self' blob:", "frame-ancestors 'none'", "base-uri 'self'", "form-action 'self'", ] ) class SecurityHeadersMiddleware(BaseHTTPMiddleware): """Add security headers to every API response.""" def __init__(self, app, environment: str = "dev"): super().__init__(app) self.is_production = environment != "dev" async def dispatch(self, request: Request, call_next) -> Response: response = await call_next(request) # Always-on security headers response.headers["X-Content-Type-Options"] = "nosniff" response.headers["X-Frame-Options"] = "DENY" response.headers["X-DNS-Prefetch-Control"] = "on" response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" response.headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=(self)" response.headers["Cache-Control"] = "no-store" # Content-Security-Policy (environment-aware) if self.is_production: response.headers["Content-Security-Policy"] = _CSP_PRODUCTION else: response.headers["Content-Security-Policy"] = _CSP_DEV # HSTS only in production (plain HTTP in dev would be blocked) if self.is_production: response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains" return response