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