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:
+1
-1
@@ -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
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user