security(web): enforce anti-clickjacking via response headers

frame-ancestors is ignored inside a <meta> 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) <noreply@anthropic.com>
This commit is contained in:
2026-07-17 10:37:25 -07:00
parent 01163891ec
commit 5896ec0893
2 changed files with 15 additions and 4 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: blob:; connect-src 'self' ws: wss:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: blob:; connect-src 'self' ws: wss:; object-src 'none'; base-uri 'none'">
<meta name="theme-color" content="#15161a" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#eceef4" media="(prefers-color-scheme: light)">
<meta name="description" content="Remote macro control for your desktop">
+14 -3
View File
@@ -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 <meta> 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():