fix(dashboard): allow packaged desktop file:// origin on loopback WS

The packaged Electron desktop loads its renderer over file://, so its
/api/ws handshake carries Origin: file:// (or null). The DNS-rebinding
WebSocket Origin guard only accepted http(s) origins matching the bound
host, so it rejected the desktop's own renderer with 4403 -> "Could not
connect to Hermes gateway" on macOS.

A browser DNS-rebinding attacker can only ever present an http(s) origin
(the site hosting the malicious page); it cannot forge file://, null, or
a custom app scheme AND hold the loopback session token. So on loopback
binds we now trust non-web origins -- the token in _ws_auth_ok remains
the real authenticator. Public/gated binds still reject them, and
cross-site http(s) origins are still rejected everywhere.
This commit is contained in:
Brooklyn Nicholson
2026-05-30 01:40:35 -05:00
parent 5aade4bc57
commit 861b63228c
2 changed files with 54 additions and 1 deletions
+10 -1
View File
@@ -4210,7 +4210,16 @@ def _ws_host_origin_is_allowed(ws: "WebSocket") -> bool:
return True
parsed = urllib.parse.urlparse(origin)
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
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
if not parsed.netloc:
return False
return _is_accepted_host(parsed.netloc, bound_host)