From 5896ec0893b362c6081e5d664ce1adb77cf346d5 Mon Sep 17 00:00:00 2001 From: MarcoPad Dev Date: Fri, 17 Jul 2026 10:37:25 -0700 Subject: [PATCH] security(web): enforce anti-clickjacking via response headers frame-ancestors is ignored inside a CSP, so move clickjacking protection to real response headers (X-Frame-Options: DENY) and add X-Content-Type-Options: nosniff and Referrer-Policy: no-referrer on all responses. Drop the ineffective frame-ancestors token from the meta CSP. Co-Authored-By: Claude Opus 4.8 (1M context) --- web/index.html | 2 +- web_server.py | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) 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():