NeighbourAid / app /core /security_headers.py
Parth Kansal
commit
49e9f9d
"""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