fix(gui): return 204 for browser-noise paths to prevent CSRF races

Browser requests for /favicon.ico, /apple-touch-icon.png, etc. were
triggering parallel GET requests that could race with form loads,
causing CSRF token rotation issues.

Added BROWSER_NOISE_PATHS constant and early 204 response in both
SetupGateMiddleware and SessionMiddleware to short-circuit these
requests before any cookie/token handling occurs.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Matt Johnson 2026-05-18 07:34:41 +00:00
commit 455470b0c9

View file

@ -18,6 +18,14 @@ SETUP_EXEMPT_PREFIXES = ("/static/", "/setup")
AUTH_EXEMPT_PATHS = {"/setup/operator", "/login", "/health"} AUTH_EXEMPT_PATHS = {"/setup/operator", "/login", "/health"}
AUTH_EXEMPT_PREFIXES = ("/static/", "/setup/") AUTH_EXEMPT_PREFIXES = ("/static/", "/setup/")
# Browser-noise paths that trigger CSRF race conditions
BROWSER_NOISE_PATHS = {
"/favicon.ico",
"/apple-touch-icon.png",
"/apple-touch-icon-precomposed.png",
"/robots.txt",
}
def _is_exempt(path: str, exempt_paths: set, exempt_prefixes: tuple) -> bool: def _is_exempt(path: str, exempt_paths: set, exempt_prefixes: tuple) -> bool:
"""Check if a path is exempt from a check.""" """Check if a path is exempt from a check."""
@ -45,6 +53,10 @@ class SetupGateMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next) -> Response: async def dispatch(self, request: Request, call_next) -> Response:
path = request.url.path path = request.url.path
# Short-circuit browser-noise requests that cause CSRF races
if path in BROWSER_NOISE_PATHS:
return Response(status_code=204)
# Check setup status from database # Check setup status from database
pool = get_pool() pool = get_pool()
if pool is None: if pool is None:
@ -102,6 +114,11 @@ class SessionMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next) -> Response: async def dispatch(self, request: Request, call_next) -> Response:
path = request.url.path path = request.url.path
# Short-circuit browser-noise requests (already handled by SetupGateMiddleware,
# but this protects if middleware order changes)
if path in BROWSER_NOISE_PATHS:
return Response(status_code=204)
# Initialize state # Initialize state
request.state.operator = None request.state.operator = None
request.state.csrf_token = None request.state.csrf_token = None