Skip to content

fix(web_server): propagate --insecure flag to WebSocket security checks#31772

Closed
pranvgarg wants to merge 1 commit into
NousResearch:mainfrom
pranvgarg:fix/insecure-flag-ws-propagation
Closed

fix(web_server): propagate --insecure flag to WebSocket security checks#31772
pranvgarg wants to merge 1 commit into
NousResearch:mainfrom
pranvgarg:fix/insecure-flag-ws-propagation

Conversation

@pranvgarg

Copy link
Copy Markdown

Problem

When the dashboard is bound to a non-loopback address using --insecure (e.g. a Tailscale IP like 100.116.24.10), all WebSocket connections to /api/pty, /api/ws, /api/pub, and /api/events fail with 403 Forbidden.

HTTP endpoints work fine. Only WebSockets are broken.

Root cause

start_server() accepts allow_public: bool (set when --insecure is passed) and uses it to skip the startup SystemExit guard — but never writes it to app.state:

# before
app.state.bound_host = host
app.state.bound_port = port
# app.state.insecure  ← never set

Every getattr(app.state, "insecure", False) call in the security guards always evaluates to False, making --insecure a no-op for WebSocket connections.

Specifically, _ws_client_is_allowed() checks the peer IP against _LOOPBACK_HOSTS. When the server is bound to a non-loopback address, the connecting client also has a non-loopback IP — so the check always fails and the WebSocket is rejected.

Fix

  • Set app.state.insecure = allow_public in start_server() so the flag is visible to all handlers at runtime.
  • Add the insecure short-circuit to _ws_client_is_allowed() and _ws_host_origin_is_allowed(), consistent with the existing (but unreachable) guard already present in host_header_middleware.
  • Correct host_header_middleware's insecure branch to return await call_next(request) instead of bare True — returning a non-Response from an ASGI middleware causes a runtime TypeError.

Verification

Tested with hermes dashboard --host <tailscale-ip> --insecure. Before this fix, /api/events and /api/pty both returned 403. After this fix, both WebSocket connections complete the handshake and the dashboard chat tab works correctly.

start_server() stored allow_public in a local variable but never wrote it
to app.state, so every getattr(app.state, "insecure", False) call in the
security guards always evaluated to False — making the --insecure flag a
no-op for WebSocket connections.

Symptoms: when the dashboard is bound to a non-loopback address via
--insecure (e.g. a Tailscale IP), all WebSocket upgrades to /api/pty,
/api/ws, /api/pub, and /api/events were rejected with 403 because
_ws_client_is_allowed() saw a non-loopback peer IP and returned False.
HTTP endpoints worked fine because _is_accepted_host() matched the bound
host directly.

Fix:
- Set app.state.insecure = allow_public in start_server() so the flag is
  visible to all request handlers at runtime.
- Add the corresponding insecure short-circuit to _ws_client_is_allowed()
  and _ws_host_origin_is_allowed(), consistent with the existing guard in
  host_header_middleware.
- Correct host_header_middleware's insecure branch to return
  await call_next(request) instead of bare True (returning a non-Response
  from ASGI middleware causes a runtime error).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@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 25, 2026
@pranvgarg

Copy link
Copy Markdown
Author

Closing to revise — will reopen with a cleaner version.

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.

2 participants