Spaces:
Running
Running
| """Security-headers middleware. | |
| Adds the OWASP-recommended response headers to every API response. | |
| Most of them have no runtime cost; they exist purely so a browser | |
| treats responses safely (no MIME-sniffing, no clickjacking, no | |
| referrer leak, locked-down feature set, only-https-after-first-visit). | |
| We deliberately do NOT set a Content-Security-Policy here — the API | |
| returns JSON, never HTML, so CSP would be moot. The frontend's CSP | |
| is enforced by the SPA host (nginx/Vercel/HF Spaces serving the | |
| built bundle). | |
| """ | |
| from __future__ import annotations | |
| from starlette.middleware.base import BaseHTTPMiddleware | |
| from starlette.requests import Request | |
| # Header set chosen for an API server. Tightened to what makes sense for | |
| # JSON-only endpoints; the frontend layer adds its own CSP/COOP/COEP | |
| # at the edge (nginx / Vercel / Cloudflare). | |
| _HEADERS: dict[str, str] = { | |
| # Prevents MIME-sniffing attacks. Browsers should treat the | |
| # Content-Type response header as authoritative. | |
| "X-Content-Type-Options": "nosniff", | |
| # Defence-in-depth against clickjacking. The API returns JSON, | |
| # but HTML error pages from misconfigured proxies might be framed. | |
| "X-Frame-Options": "DENY", | |
| # Hide the path/query of the API URL when a browser navigates | |
| # cross-origin from one of our pages. | |
| "Referrer-Policy": "strict-origin-when-cross-origin", | |
| # Restrict access to powerful browser APIs from any code we serve. | |
| # The frontend uses geolocation + microphone + camera, which are | |
| # explicitly granted via the SPA's own permission prompts; the API | |
| # never needs them. | |
| "Permissions-Policy": ( | |
| "camera=(self), microphone=(self), geolocation=(self), " | |
| "payment=(), usb=(), accelerometer=(), gyroscope=(), magnetometer=()" | |
| ), | |
| # Strict Transport Security: tell browsers to always upgrade | |
| # http:// → https:// for our origin for the next 6 months. Free | |
| # hosts (HF Spaces, Vercel) terminate TLS for us so this is safe | |
| # to set unconditionally; if you ever serve plain HTTP behind a | |
| # custom domain you'd want to gate this on `request.url.scheme`. | |
| "Strict-Transport-Security": "max-age=15552000; includeSubDomains", | |
| # Cross-Origin-Resource-Policy. `cross-origin` allows the SPA on | |
| # any domain to call us; we already gate auth via JWT in the | |
| # Authorization header, which is not subject to CSRF the way | |
| # cookie-auth is. | |
| "Cross-Origin-Resource-Policy": "cross-origin", | |
| } | |
| class SecurityHeadersMiddleware(BaseHTTPMiddleware): | |
| async def dispatch(self, request: Request, call_next): | |
| response = await call_next(request) | |
| for name, value in _HEADERS.items(): | |
| # Don't clobber explicit per-route overrides. | |
| response.headers.setdefault(name, value) | |
| return response | |