yusufcalisir's picture
deploy: Hugging Face space upload
eb0499d
Raw History Blame Contribute Delete
4.86 kB
"""HTTP Security Headers Middleware.
Injects industry-standard defensive HTTP response headers on every outgoing
response to prevent a broad class of client-side attacks:
- Content-Security-Policy : restricts permitted script/style/connect sources
- Strict-Transport-Security: enforces HTTPS for 1 year + subdomains + preload
- X-Content-Type-Options : prevents MIME-type sniffing (nosniff)
- X-Frame-Options : blocks clickjacking via iframe embedding (DENY)
- Referrer-Policy : limits referrer leakage to same-origin only
- Permissions-Policy : disables powerful browser features not in use
- X-XSS-Protection : legacy IE/Edge XSS filter (belt-and-suspenders)
FastAPI / Starlette variant - wraps every response via BaseHTTPMiddleware.
"""
from __future__ import annotations
from starlette.datastructures import MutableHeaders
from starlette.types import ASGIApp, Message, Receive, Scope, Send
# ---------------------------------------------------------------------------
# Content-Security-Policy directive values
# ---------------------------------------------------------------------------
# API-only backend: no inline scripts or styles are served.
# WebSocket connections are allowed via wss:// (same-origin or explicit host).
# Keep it strict - the frontend is a separate Vite/Vercel SPA that sets its
# own CSP either via Vercel config or a meta tag.
_CSP_DIRECTIVES = "; ".join(
[
"default-src 'none'",
"script-src 'none'",
"style-src 'none'",
"img-src 'none'",
"font-src 'none'",
"connect-src 'self'",
"frame-ancestors 'none'",
"form-action 'none'",
"base-uri 'none'",
"object-src 'none'",
]
)
_DOCS_CSP_DIRECTIVES = "; ".join(
[
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: https://cdn.jsdelivr.net https://unpkg.com https://*",
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://unpkg.com https://*",
"img-src 'self' data: blob: https://fastapi.tiangolo.com https://cdn.jsdelivr.net https://scalar.com https://*",
"font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net https://* data: blob:",
"connect-src 'self' https://cdn.jsdelivr.net https://* wss://*",
"worker-src 'self' blob: https://*",
"child-src 'self' blob: https://*",
"frame-ancestors 'none'",
"form-action 'self'",
"base-uri 'self'",
]
)
_SECURITY_HEADERS: dict[str, str] = {
# Prevents all external resource loading; this is an API backend
"Content-Security-Policy": _CSP_DIRECTIVES,
# HTTPS mandatory for 1 year; add subdomain coverage + signal preload-list readiness
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
# Block MIME-type sniffing attacks
"X-Content-Type-Options": "nosniff",
# Deny all iframe embedding - eliminates clickjacking surface
"X-Frame-Options": "DENY",
# No referrer information is sent cross-origin - minimises PII leakage
"Referrer-Policy": "no-referrer",
# Disable potentially sensitive browser features not required by this API
"Permissions-Policy": "geolocation=(), microphone=(), camera=(), payment=()",
# Legacy IE/Edge XSS filter (belt-and-suspenders; modern browsers ignore it)
"X-XSS-Protection": "1; mode=block",
}
class SecurityHeadersMiddleware:
"""Starlette/FastAPI pure ASGI middleware that appends security headers to every response.
Existing headers set by individual routes are preserved; docs routes (/docs,
/redoc, /scalar) receive tailored CSP headers allowing Swagger and Scalar CDN assets.
"""
def __init__(self, app: ASGIApp) -> None:
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] != "http":
await self.app(scope, receive, send)
return
path = scope.get("path", "")
is_docs_route = path in ("/openapi.json", "/favicon.ico", "/favicon.svg") or any(
path.startswith(p) for p in ("/docs", "/redoc", "/scalar", "/logo")
)
headers_to_apply = dict(_SECURITY_HEADERS)
if is_docs_route:
headers_to_apply["Content-Security-Policy"] = _DOCS_CSP_DIRECTIVES
async def send_wrapper(message: Message) -> None:
if message["type"] == "http.response.start":
headers = MutableHeaders(scope=message)
for header, value in headers_to_apply.items():
if header not in headers or (is_docs_route and header == "Content-Security-Policy"):
headers[header] = value
await send(message)
await self.app(scope, receive, send_wrapper)