fix(ws): accept same-origin WebSocket connections in default Origin allow-list
When `cors_origins` was unset, the WS endpoint only allowed `http://localhost:<port>` and `http://127.0.0.1:<port>` 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.
This commit is contained in:
@@ -414,8 +414,13 @@ async def websocket_endpoint(
|
|||||||
accept_subprotocol = proto
|
accept_subprotocol = proto
|
||||||
break
|
break
|
||||||
effective_token = subprotocol_token or token
|
effective_token = subprotocol_token or token
|
||||||
# Origin check — block CSWSH from third-party LAN pages. We accept the same
|
# Origin check — block CSWSH from third-party LAN pages. Accept the same
|
||||||
# set of origins as CORS plus the default localhost loopback.
|
# 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(
|
allowed_origins = set(
|
||||||
settings.cors_origins
|
settings.cors_origins
|
||||||
or [
|
or [
|
||||||
@@ -427,8 +432,17 @@ async def websocket_endpoint(
|
|||||||
# Same-origin connections from native apps may omit Origin entirely; only
|
# Same-origin connections from native apps may omit Origin entirely; only
|
||||||
# reject when an Origin is present AND not in the allow-list.
|
# reject when an Origin is present AND not in the allow-list.
|
||||||
if origin is not None and origin not in allowed_origins:
|
if origin is not None and origin not in allowed_origins:
|
||||||
await websocket.close(code=4003, reason="Origin not allowed")
|
host_header = websocket.headers.get("host", "")
|
||||||
return
|
# 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
|
# Verify token
|
||||||
from ..auth import auth_enabled, get_token_label, token_label_var
|
from ..auth import auth_enabled, get_token_label, token_label_var
|
||||||
|
|||||||
Reference in New Issue
Block a user