"""Security headers middleware. Emits a conservative CSP that allows only self + jsdelivr (Chart.js) and standard hardening headers. HSTS only when behind HTTPS proxy. """ from __future__ import annotations from starlette.middleware.base import BaseHTTPMiddleware from starlette.types import ASGIApp from .settings import settings CSP_DIRECTIVES = ( "default-src 'self'; " "script-src 'self' https://cdn.jsdelivr.net; " "style-src 'self' 'unsafe-inline'; " "img-src 'self' data:; " "font-src 'self' data:; " "connect-src 'self'; " "base-uri 'self'; " "form-action 'self'; " "frame-ancestors 'none'; " "object-src 'none'; " "upgrade-insecure-requests" ) class SecurityHeadersMiddleware(BaseHTTPMiddleware): def __init__(self, app: ASGIApp): super().__init__(app) async def dispatch(self, request, call_next): response = await call_next(request) response.headers.setdefault("X-Content-Type-Options", "nosniff") response.headers.setdefault("X-Frame-Options", "DENY") response.headers.setdefault("Referrer-Policy", "same-origin") response.headers.setdefault("Content-Security-Policy", CSP_DIRECTIVES) response.headers.setdefault( "Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=()", ) response.headers.setdefault("Cross-Origin-Opener-Policy", "same-origin") if settings.trust_proxy: response.headers.setdefault( "Strict-Transport-Security", "max-age=31536000; includeSubDomains", ) return response