mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-12 08:51:53 +00:00
fix(dashboard): allow desktop websocket origins on remote binds
This commit is contained in:
parent
54343bcade
commit
6ed9a2de8f
2 changed files with 35 additions and 10 deletions
|
|
@ -6605,13 +6605,16 @@ def _ws_host_origin_is_allowed(ws: "WebSocket") -> bool:
|
|||
|
||||
parsed = urllib.parse.urlparse(origin)
|
||||
if parsed.scheme not in {"http", "https"}:
|
||||
# Packaged Electron loads the desktop renderer over file://, so its
|
||||
# WebSocket handshake carries a non-web Origin such as file:// or null.
|
||||
# DNS-rebinding attacks originate from an http(s) site; they cannot
|
||||
# forge a file:// origin and still hold the loopback session token.
|
||||
# Public/gated binds have no legitimate non-web client, so keep
|
||||
# rejecting these origins there.
|
||||
return bound_host.lower() in _LOOPBACK_HOST_VALUES
|
||||
# Packaged Electron loads the desktop renderer over a non-web origin
|
||||
# such as file:// or null. This helper is called only after _ws_auth_ok
|
||||
# has accepted the WS credential; in non-gated mode that credential is
|
||||
# the legacy dashboard session token, including for explicit Tailscale /
|
||||
# LAN binds opened with --insecure. Real DNS-rebinding attacks arrive
|
||||
# from http(s) origins and still have to match the bound host below.
|
||||
#
|
||||
# OAuth-gated public dashboards authenticate with cookies/tickets and
|
||||
# have no legitimate file:// client, so keep them strict.
|
||||
return not getattr(app.state, "auth_required", False)
|
||||
|
||||
if not parsed.netloc:
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -381,8 +381,9 @@ class TestWsHostOriginGuardOrigins:
|
|||
Electron loads the packaged renderer over ``file://``, so its WebSocket
|
||||
handshake carries ``Origin: file://`` (or the opaque ``null``). The
|
||||
DNS-rebinding guard only needs to block cross-site http(s) origins. On a
|
||||
loopback bind these non-web origins are trusted because the session token
|
||||
is the real gate. Public/gated binds keep rejecting them.
|
||||
loopback or explicit non-loopback insecure bind these non-web origins are
|
||||
trusted because the session token is the real gate. OAuth-gated public
|
||||
binds keep rejecting them.
|
||||
"""
|
||||
|
||||
def _ws(self, *, origin, host):
|
||||
|
|
@ -413,8 +414,29 @@ class TestWsHostOriginGuardOrigins:
|
|||
ws = self._ws(origin="http://evil.test", host="127.0.0.1:8080")
|
||||
assert web_server._ws_host_origin_is_allowed(ws) is False
|
||||
|
||||
def test_explicit_non_loopback_file_origin_allowed(self, insecure_explicit_host_app):
|
||||
"""Packaged Hermes Desktop also uses file:// when connecting to a
|
||||
Tailscale/LAN dashboard bind.
|
||||
|
||||
The WebSocket route calls _ws_auth_ok before this guard, so in
|
||||
non-gated mode the legacy session token remains the auth boundary.
|
||||
"""
|
||||
ws = self._ws(origin="file://", host="100.64.0.10:9119")
|
||||
assert web_server._ws_host_origin_is_allowed(ws) is True
|
||||
|
||||
def test_explicit_non_loopback_null_origin_allowed(self, insecure_explicit_host_app):
|
||||
ws = self._ws(origin="null", host="100.64.0.10:9119")
|
||||
assert web_server._ws_host_origin_is_allowed(ws) is True
|
||||
|
||||
def test_explicit_non_loopback_cross_site_http_origin_rejected(
|
||||
self, insecure_explicit_host_app
|
||||
):
|
||||
ws = self._ws(origin="http://localhost:9119", host="100.64.0.10:9119")
|
||||
assert web_server._ws_host_origin_is_allowed(ws) is False
|
||||
|
||||
def test_gated_file_origin_rejected(self, gated_app):
|
||||
# A public/gated bind has no legitimate file:// client.
|
||||
# OAuth-gated public dashboards authenticate with cookies/tickets,
|
||||
# not the legacy desktop session token.
|
||||
ws = self._ws(origin="file://", host="fly-app.fly.dev")
|
||||
assert web_server._ws_host_origin_is_allowed(ws) is False
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue