Skip to content

fix(dashboard): allow WebSocket connections on non-loopback binds#25072

Closed
mrSutivu wants to merge 1 commit into
NousResearch:mainfrom
mrSutivu:fix/dashboard-websocket-auth-bypass
Closed

fix(dashboard): allow WebSocket connections on non-loopback binds#25072
mrSutivu wants to merge 1 commit into
NousResearch:mainfrom
mrSutivu:fix/dashboard-websocket-auth-bypass

Conversation

@mrSutivu

Copy link
Copy Markdown

Problem

The dashboard chat tab fails to connect when the web UI is bound to a non-loopback address (e.g. a Tailscale IP with --insecure). The button shows "reconnect" but the WebSocket never establishes. Three distinct bugs contribute to this:

1. HTTP auth middleware blocks WebSocket upgrades

The auth_middleware enforces X-Hermes-Session-Token or Authorization: Bearer headers on all /api/* routes except a hardcoded allowlist. WebSocket endpoints use query params (?token=...) because browsers cannot set custom headers during the WS upgrade. /api/pty, /api/ws, /api/events, and /api/pub were not on the allowlist, so the middleware returned 401 before the WS handler could validate the token.

2. CORS blocks non-loopback origins

The allow_origin_regex only accepted localhost and 127.0.0.1. Accessing the dashboard from a Tailscale IP (or any other non-loopback bind) failed CORS preflight checks.

3. _ws_client_is_allowed rejects non-localhost clients

This function only checked for 0.0.0.0/:: binds (_is_public_bind) when deciding whether to accept non-loopback WebSocket clients. An explicit non-loopback bind (like a specific Tailscale IP with --insecure) fell through to the localhost-only check, rejecting the connection.

Fixes

  • web_server.py lines 114-128: Add WebSocket paths to _PUBLIC_API_PATHS so the HTTP auth middleware doesnt intercept them (they validate tokens themselves via query params).
  • web_server.py line 104: Extend CORS regex to include 0.0.0.0 and Tailscale IP range (100.x.x.x).
  • web_server.py lines 3200-3218: Extend _ws_client_is_allowed to allow any client when bound_host is a specific non-loopback address.

Three issues prevented the dashboard chat tab from connecting when
the web UI is bound to a non-loopback address (e.g. Tailscale IP):

1. The HTTP auth middleware rejected WebSocket upgrade requests because
   session tokens are passed via query params (browsers can't set
   custom headers on WS upgrades). Add /api/pty, /api/ws, /api/events,
   and /api/pub to the public paths — each handler validates the token
   itself.

2. The CORS origin regex only matched localhost/127.0.0.1. Extend it
   to also allow 0.0.0.0 and Tailscale (100.x.x.x) origins.

3. _ws_client_is_allowed only checked for 0.0.0.0/:: bind but not
   explicit non-loopback addresses. Allow any client when the operator
   intentionally binds to a specific non-loopback address (--insecure).
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/cli CLI entry point, hermes_cli/, setup wizard labels May 13, 2026
@mrSutivu mrSutivu closed this May 15, 2026
@mrSutivu mrSutivu deleted the fix/dashboard-websocket-auth-bypass branch May 15, 2026 00:02
@mrSutivu mrSutivu restored the fix/dashboard-websocket-auth-bypass branch May 15, 2026 00:02
@mrSutivu mrSutivu reopened this May 15, 2026
@austinpickett austinpickett requested a review from Copilot May 18, 2026 14:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes dashboard chat WebSocket connectivity when the web UI is bound to non-loopback interfaces (e.g., Tailscale IPs with --insecure) by adjusting CORS, auth allowlisting, and WebSocket client-allow rules in the FastAPI server.

Changes:

  • Expands CORS allow_origin_regex to permit additional non-loopback origins.
  • Adds WebSocket endpoints (/api/pty, /api/ws, /api/events, /api/pub) to the auth middleware allowlist, relying on query-param token validation in the WS handlers.
  • Updates _ws_client_is_allowed to allow non-loopback WebSocket clients when the server is bound to a specific non-loopback host.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread hermes_cli/web_server.py
Comment on lines 101 to 105

app.add_middleware(
CORSMiddleware,
allow_origin_regex=r"^https?://(localhost|127\.0\.0\.1)(:\d+)?$",
allow_origin_regex=r"^https?://(localhost|127\.0\.0\.1|0\.0\.0\.0|100\.\d+\.\d+\.\d+)(:\d+)?$",
allow_methods=["*"],
Comment thread hermes_cli/web_server.py
Comment on lines 3208 to +3214
return True
# When bound to a specific non-loopback address (Tailscale IP, etc.)
# with --insecure, allow any client — the operator explicitly chose
# a non-loopback bind.
bound_host = getattr(app.state, "bound_host", "")
if bound_host and bound_host not in _LOOPBACK_HOSTS:
return True
@teknium1

Copy link
Copy Markdown
Contributor

Landed in PR #35386 (merge commit 234ac00), now on main. You flagged the same explicit-non-loopback-bind gap that #32357 did — both surfaced the scenario the wildcard fix (#35141) didn't cover. Credited as co-reporter in the merged PR body. (Couldn't cherry-pick your commit directly — it carried a generated agent@nousresearch.ai author email rather than your GitHub identity, so attribution went through the PR body instead.) Thanks for the report.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants