diff --git a/web/index.html b/web/index.html index 6d3fdfb..5d087f8 100644 --- a/web/index.html +++ b/web/index.html @@ -3,7 +3,7 @@
- + diff --git a/web_server.py b/web_server.py index 5ba71d6..9d0e10d 100644 --- a/web_server.py +++ b/web_server.py @@ -140,21 +140,32 @@ class WebServer: # Paths served without a token (PWA shell + static assets) public_paths = {"/", "/manifest.json", "/service-worker.js"} + def _add_security_headers(response): + # Anti-clickjacking / MIME-sniffing. frame-ancestors can only be set + # via a real response header (it is ignored inside a CSP), so + # we enforce it here rather than in index.html. + response.headers["X-Frame-Options"] = "DENY" + response.headers["X-Content-Type-Options"] = "nosniff" + response.headers["Referrer-Policy"] = "no-referrer" + return response + @app.middleware("http") async def auth_middleware(request: Request, call_next): """Require a valid auth token for all non-public paths.""" path = request.url.path if path in public_paths or path.startswith("/static/"): - return await call_next(request) + return _add_security_headers(await call_next(request)) # Fail-open only when no token is configured (shouldn't happen in prod) if self.auth_token: provided = request.headers.get("X-MacroPad-Token") \ or request.query_params.get("token") or "" if not hmac.compare_digest(provided, self.auth_token): - return JSONResponse({"detail": "Unauthorized"}, status_code=401) + return _add_security_headers( + JSONResponse({"detail": "Unauthorized"}, status_code=401) + ) - return await call_next(request) + return _add_security_headers(await call_next(request)) @app.get("/", response_class=HTMLResponse) async def index():