diff --git a/backend/app/main.py b/backend/app/main.py index 00c76347..afb3cd0c 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -160,6 +160,29 @@ async def static_cache_headers(request, call_next): return response +# Baseline security response headers (SEC-1). A minimal, safe set that needs no per-route tuning: +# nosniff stops MIME-confusion, the Referrer-Policy keeps the (private) app URL from leaking to +# external sites the user follows, `frame-ancestors 'none'` blocks click-jacking by refusing to be +# iframed anywhere (we never embed our own pages; we embed OTHERS, which this does not affect), and +# Permissions-Policy turns off powerful features the app never uses. A full Content-Security-Policy +# (script/style/connect sources) is deferred — the SPA's inline styles + the YouTube/Plex embeds make +# it real work — and HSTS belongs on the TLS-terminating edge (the Caddy front door), not here. +_SECURITY_HEADERS = { + "X-Content-Type-Options": "nosniff", + "Referrer-Policy": "strict-origin-when-cross-origin", + "Content-Security-Policy": "frame-ancestors 'none'", + "Permissions-Policy": "camera=(), microphone=(), geolocation=()", +} + + +@app.middleware("http") +async def security_headers(request, call_next): + response = await call_next(request) + for name, value in _SECURITY_HEADERS.items(): + response.headers.setdefault(name, value) + return response + + app.include_router(health.router) app.include_router(auth_routes.router) app.include_router(sync.router)