fix(dashboard): allow chat websockets on insecure public bind

Allow non-loopback websocket peers when the dashboard is explicitly exposed with --host 0.0.0.0/:: and --insecure.

This fixes the failure mode where /chat rendered over LAN but /api/ws and /api/events were rejected with HTTP 403, leaving the embedded TUI chat disconnected.

Add regression coverage for the insecure public bind case in the dashboard websocket auth tests.
This commit is contained in:
SeaXen
2026-05-30 00:23:44 -07:00
committed by Teknium
parent 636ff636d7
commit e8076c1ebe
2 changed files with 43 additions and 1 deletions
+9 -1
View File
@@ -3371,10 +3371,15 @@ _LOOPBACK_HOSTS = frozenset({"127.0.0.1", "::1", "localhost", "testclient"})
def _ws_client_is_allowed(ws: "WebSocket") -> bool:
"""Check if the WebSocket client IP is acceptable.
Loopback mode: only loopback clients allowed — the legacy
Loopback bind: only loopback clients allowed — the legacy
``?token=<_SESSION_TOKEN>`` path is the only auth we have, so we
don't want LAN hosts guessing tokens.
All-interfaces insecure bind (``--host 0.0.0.0 --insecure`` or
``--host :: --insecure``): allow any peer. The operator explicitly
opted into LAN/public exposure in this mode, so the loopback-only peer
restriction should not apply.
Gated mode: any peer is allowed — uvicorn's ``proxy_headers=True``
(enabled when the OAuth gate is active so cookies can pick up
``X-Forwarded-Proto``) rewrites ``ws.client.host`` to the
@@ -3385,6 +3390,9 @@ def _ws_client_is_allowed(ws: "WebSocket") -> bool:
"""
if getattr(app.state, "auth_required", False):
return True
bound_host = getattr(app.state, "bound_host", "")
if bound_host in {"0.0.0.0", "::"}:
return True
client_host = ws.client.host if ws.client else ""
if not client_host:
return True