From 9b9a2b5c9f41eb46af67bbf53ed566132d9a6b63 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Mon, 25 May 2026 23:44:57 +0300 Subject: [PATCH] fix(ws): accept same-origin WebSocket connections in default Origin allow-list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When `cors_origins` was unset, the WS endpoint only allowed `http://localhost:` and `http://127.0.0.1:` as origins, so a browser opening the UI via the LAN IP (e.g. `http://192.168.2.100:8765` when bound to `0.0.0.0`) had its WebSocket closed with code 4003 and never recovered — leaving the Web UI in a permanent reconnect loop. Also accept any `Origin` whose authority matches the request's `Host` header (both `http://` and `https://` schemes). Same-origin is by definition not CSWSH, so the cross-origin defence added in v0.3.0 remains intact for genuine third-party LAN pages. --- media_server/routes/media.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/media_server/routes/media.py b/media_server/routes/media.py index 1099d27..c1db1e9 100644 --- a/media_server/routes/media.py +++ b/media_server/routes/media.py @@ -414,8 +414,13 @@ async def websocket_endpoint( accept_subprotocol = proto break effective_token = subprotocol_token or token - # Origin check — block CSWSH from third-party LAN pages. We accept the same - # set of origins as CORS plus the default localhost loopback. + # Origin check — block CSWSH from third-party LAN pages. Accept the same + # set of origins as CORS plus the default localhost loopback, AND any + # same-origin connection (where Origin matches the request's Host header). + # Same-origin is inherently safe from CSWSH because CSWSH is a *cross*- + # origin attack — without this, binding to 0.0.0.0 and accessing the UI + # via a LAN IP would have its WebSocket rejected by the browser-sent + # Origin, which the static allowlist can't anticipate. allowed_origins = set( settings.cors_origins or [ @@ -427,8 +432,17 @@ async def websocket_endpoint( # Same-origin connections from native apps may omit Origin entirely; only # reject when an Origin is present AND not in the allow-list. if origin is not None and origin not in allowed_origins: - await websocket.close(code=4003, reason="Origin not allowed") - return + host_header = websocket.headers.get("host", "") + # Origin uses http/https; match against both scheme variants of Host + # so HTTPS deployments without an explicit cors_origins still work. + same_origin_candidates = ( + {f"http://{host_header}", f"https://{host_header}"} + if host_header + else set() + ) + if origin not in same_origin_candidates: + await websocket.close(code=4003, reason="Origin not allowed") + return # Verify token from ..auth import auth_enabled, get_token_label, token_label_var