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>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
<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="#15161a" media="(prefers-color-scheme: dark)">
|
||||||
<meta name="theme-color" content="#eceef4" media="(prefers-color-scheme: light)">
|
<meta name="theme-color" content="#eceef4" media="(prefers-color-scheme: light)">
|
||||||
<meta name="description" content="Remote macro control for your desktop">
|
<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)
|
# Paths served without a token (PWA shell + static assets)
|
||||||
public_paths = {"/", "/manifest.json", "/service-worker.js"}
|
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")
|
@app.middleware("http")
|
||||||
async def auth_middleware(request: Request, call_next):
|
async def auth_middleware(request: Request, call_next):
|
||||||
"""Require a valid auth token for all non-public paths."""
|
"""Require a valid auth token for all non-public paths."""
|
||||||
path = request.url.path
|
path = request.url.path
|
||||||
if path in public_paths or path.startswith("/static/"):
|
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)
|
# Fail-open only when no token is configured (shouldn't happen in prod)
|
||||||
if self.auth_token:
|
if self.auth_token:
|
||||||
provided = request.headers.get("X-MacroPad-Token") \
|
provided = request.headers.get("X-MacroPad-Token") \
|
||||||
or request.query_params.get("token") or ""
|
or request.query_params.get("token") or ""
|
||||||
if not hmac.compare_digest(provided, self.auth_token):
|
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)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
async def index():
|
async def index():
|
||||||
|
|||||||
Reference in New Issue
Block a user