Skip to content

Hermes Desktop remote mode rejects Electron WebSocket origins on non-loopback dashboard binds #37399

@leonardsellem

Description

@leonardsellem

Bug

Hermes Desktop remote mode can report the remote backend as ready, then fail during renderer boot with:

Could not connect to Hermes gateway

This happens when the dashboard is exposed on an explicit non-loopback address, for example a Tailscale/LAN bind such as --host 100.64.x.y --insecure --tui.

Root cause

There are two related gaps in the remote path:

  1. Desktop remote readiness checks /api/status, which is public. That can pass even when the saved dashboard session token is stale or unusable for the protected endpoints and WebSocket path the renderer actually needs.
  2. When the packaged Electron renderer opens /api/ws, Chromium sends a non-web WebSocket Origin such as file:// or null. _ws_host_origin_is_allowed() currently allows those non-web origins only for loopback binds. For an explicit non-loopback bind, the token is accepted first, but the WebSocket is then rejected by the Host/Origin guard.

The result is a confusing split-brain state: desktop.log says Remote Hermes backend is ready, but the UI shows the boot failure overlay.

Reproduction

  1. Run a dashboard on a Tailscale/LAN address:

    hermes dashboard --host 100.64.0.10 --port 9119 --insecure --no-open --tui
  2. Configure Hermes Desktop remote mode to http://100.64.0.10:9119 with the injected dashboard session token.

  3. Start Hermes Desktop.

Expected: the desktop renderer connects to ws://100.64.0.10:9119/api/ws?token=... and boots.

Actual: /api/status passes, then gateway.connect() fails because the WebSocket upgrade is rejected when Electron sends Origin: file:// or Origin: null.

Security expectation

The fix should not relax normal browser DNS-rebinding protection:

  • Matching http(s) origins should continue to be required for web origins.
  • Mismatched web origins such as http://localhost:9119 against a Tailscale host should still be rejected.
  • OAuth-gated public dashboards should keep rejecting non-web origins.
  • The non-web origin allowance should only apply to the legacy token-authenticated remote desktop/dashboard path, after token auth has already succeeded.

Related local evidence

On a Tailscale-bound dashboard, a direct WebSocket test reproduced the exact split:

  • no Origin: open
  • Origin: http://<tailscale-host>:9119: open
  • Origin: file://: rejected before fix
  • Origin: null: rejected before fix
  • mismatched Origin: http://localhost:9119: rejected as expected

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium — degraded but workaround existsarea/authAuthentication, OAuth, credential poolscomp/gatewayGateway runner, session dispatch, deliverytype/bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions