fix(dashboard): allow desktop websocket origins on remote binds - #37405
fix(dashboard): allow desktop websocket origins on remote binds#37405leonardsellem wants to merge 1 commit into
Conversation
Morad37
left a comment
There was a problem hiding this comment.
This looks right to me. The important bit is that non-web origins are still only accepted after the WebSocket credential gate, while http(s) origins still go through the host match. That keeps the DNS rebinding protection intact for browser-origin traffic and fixes the packaged Desktop remote case.
The tests cover the three cases I would care about: file/null on explicit insecure host, cross-site http still rejected, and OAuth-gated public dashboard still strict.
Independent reproduction + LGTMSecond independent verification of this regression and of this fix's correctness. Reproduction environment
Same symptoms as the OP — Live-server verification of the root causeDirectly patching the running container's Same for Security regression checkWith the same patch applied, One additional data pointOAuth-gated public dashboards should correctly keep rejecting non-web origins — which this PR does because EndorsementLGTM — this is the minimal correct fix. Please merge. The Desktop experience on Tailscale/LAN remote-gateway setups is currently broken for every user who upgrades past v0.13 + v0.14; this PR restores it without weakening rebinding defense or gated-mode security. Cross-references
|
There was a problem hiding this comment.
Pull request overview
This PR fixes Hermes Desktop “remote mode” WebSocket failures when the dashboard is bound to an explicit non-loopback address (e.g., Tailscale/LAN IP). It updates the dashboard WebSocket Origin guard to accept Electron’s non-web origins (file:// / null) on non-gated (--insecure) binds, while keeping OAuth-gated/public dashboards and real http(s) origin matching strict to preserve DNS-rebinding protections.
Changes:
- Relax
_ws_host_origin_is_allowed()for non-http(s)WebSocket origins whenapp.state.auth_requiredis false (non-gated /--insecuremode), enabling authenticated Desktop connections on explicit non-loopback binds. - Preserve strict Origin validation for OAuth-gated/public dashboards and for
http(s)origins (must match the bound host). - Add regression tests covering
file://andnullorigins on explicit non-loopback insecure binds, and ensuring mismatchedhttp://localhost:...origins are still rejected.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
hermes_cli/web_server.py |
Allows non-web (file:// / null) WS origins in non-gated mode after WS credential auth, while keeping gated mode and http(s) origin checks strict. |
tests/hermes_cli/test_dashboard_auth_ws_auth.py |
Adds/updates regression coverage for explicit non-loopback insecure binds accepting Electron origins and rejecting mismatched http(s) origins. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
I reproduced this against a real remote Desktop setup and this PR matches the failure mode. Setup:
Observed before the fix:
After applying the same host-side change locally, the same WebSocket probe returned This looks like the right layer to fix it: Non-blocking test suggestion: a route-level |
|
Thanks for the independent reproductions @DogaOztuzun and @RobertoVillegas. The file:// origin case on non-loopback binds is exactly the failure path I was targeting. Good call on the regression test for /api/ws with valid token + file:// origin on a non-loopback bind. Will add that as a follow-up — want to get this fix out first since it blocks remote Desktop users. |
|
Thanks for the thorough diagnosis and fix, @leonardsellem — your analysis of the split-brain state (public Closing as already fixed: the same behavioral change landed on The linked issue #37399 is already closed by that work. No action needed on your end — appreciate the report and the clean repro. |
What does this PR do?
Fixes Hermes Desktop remote mode when the dashboard is bound to an explicit non-loopback address, such as a Tailscale or LAN IP with
--host ... --insecure --tui.The packaged Electron app opens
/api/wswith a non-web Origin (file://ornull). The WebSocket handler already validates credentials first with_ws_auth_ok, but the later origin guard only trusted those non-web origins on loopback binds. That made remote Desktop sessions fail even with a valid dashboard session token.This keeps the existing strict behavior for OAuth-gated public dashboards and for real
http(s)origins, while allowing authenticated non-web Electron origins on non-gated dashboard binds.Related Issue
Fixes #37399
Type of Change
Changes Made
hermes_cli/web_server.py: allow non-web WebSocket origins (file:///null) after WebSocket credentials have already authenticated, whenapp.state.auth_requiredis false.hermes_cli/web_server.py: keep OAuth-gated public dashboards strict, and keephttp(s)origins subject to the bound-host match.tests/hermes_cli/test_dashboard_auth_ws_auth.py: add regression coverage for explicit non-loopback binds acceptingfile://andnullorigins.tests/hermes_cli/test_dashboard_auth_ws_auth.py: add coverage that an explicit non-loopback bind still rejects a mismatchedhttp://localhost:...origin.How to Test
file://andnulltests failed.python -m pytest tests/hermes_cli/test_dashboard_auth_ws_auth.py::TestWsHostOriginGuardOrigins -q->9 passed.python -m pytest tests/hermes_cli/test_web_server_host_header.py tests/hermes_cli/test_dashboard_auth_ws_auth.py -q->44 passed.scripts/run_tests.sh tests/hermes_cli/test_dashboard_auth_ws_auth.py->33/33 passed.scripts/run_tests.sh -j 4->28092 passed, 26 failedacross 10 unrelated files on macOS.tests/hermes_cli/test_dashboard_auth_ws_auth.pypassed in the full wrapper run.tests/agent/test_anthropic_adapter.py(5)tests/gateway/test_gateway_shutdown.py(1)tests/gateway/test_shutdown_forensics.py(1)tests/hermes_cli/test_gateway_wsl.py(2)tests/hermes_cli/test_gateway_service.py(6)tests/hermes_cli/test_service_manager.py(2)tests/hermes_cli/test_signal_handler_kanban_worker.py(1)tests/test_live_system_guard_self_test.py(4)tests/test_tui_gateway_server.py(1)tests/tools/test_file_tools.py(3)Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/AScreenshots / Logs
The relevant runtime symptom was a Hermes Desktop WebSocket failure on macOS remote mode. Local log evidence showed the WebSocket request reaching the remote dashboard with:
Origin=file://from the packaged Electron app.token=...credential in the WebSocket URL.The new regression tests cover that token-authenticated Electron-origin path without including any secret values.