v2.1.0
Emby Watch Party v2.1.0
A security release. Two authorization gaps are closed, and because the stricter gating can now refuse requests that used to succeed, the UI gained the banners needed to explain itself instead of leaving you staring at a dead player.
Nothing you have to configure, no .env changes, no migration. Upgrade, restart, done.
Security
/hls/...now requires the party-bound session cookie. These were the only browser-facing routes with no session gate: possession of the URL was the entire credential, and an HLS URL leaks easily through browser history, theRefererheader, reverse-proxy access logs, and copy-as-cURL. They are now gated byrequire_host_token, the same gate/api/imageand/api/subtitleshave used since 2.0.0. This is what CHANGELOG 2.0.0's breaking-change note andhls.py's own module docstring have described all along;git log -S require_host_token -- backend/src/routers/hls.pyreturns nothing, so the gate was documented but never actually applied.- The cookie's party and the stream token's party must now agree. Adding the gate alone would have been close to cosmetic.
require_host_tokenresolves a party from the cookie; the HLS proxy resolved one from the URL token and used that party's host credentials to sign the upstream Emby call. Nothing compared them, so both gates were independently satisfiable by different parties: a leaked token for a private party plus a session cookie from any open party streamed the private party's content under its host's Emby token. Verified during development that with the gate in place but the match assert removed, the cross-party request returns 200 and serves the playlist. - A scraped
client_idno longer confers host or admin rights. Host identity was established by matchingclient_idalone, buthost_client_idis broadcast to every member in thehost_changedevent, andPOST /api/party/<id>/joinstores whateverclient_idthe caller supplies. Any attendee could therefore read the host's id off the broadcast, re-join supplying it, receive a validly signed session cookie carrying the host's identity, and reach/api/admin/configwith full read/write whenever the host's Emby account hadIsAdministrator=true. No leaked cookie and no network position were needed, only being in the party, which is the normal state for every viewer. Host identity is now proved byhost_session_grant, a 256-bit secret minted server-side byset_host, written only to the real host's cookie, never broadcast, and compared withcompare_digest. It is rotated on every promotion and cleared onclear_host, so a previous host's cookie stops proving anything the moment someone else takes over. The same check now guards host reclaim over Socket.IO, whose docstring had claimed a cookie-proof protection that this bypass defeated. Found by dnordel while reviewing #45; the flaw predates 2.1.0 and shipped in every 2.0.x release. - The Emby admin token is no longer stored in the session cookie. Starlette's
SessionMiddlewaresigns the cookie but does not encrypt it, so the payload isbase64(json)and anyone holding the cookie could decode it and recover a full Emby administrator access token, with no secret and no server access. That token grants control of the whole Emby server, far beyond Watch Party. Credentials now live in a server-sideAdminSessionStorewith only an opaque handle in the cookie, mirroring howhost_access_tokenhas always been kept server-side. Not XSS-reachable (the cookie ishttponly); the realistic exposure was proxy and CDN logs that capture headers, infostealers scraping browser cookie jars, and plaintext on the wire whereverSESSION_COOKIE_SECURE=false. Admin logout now destroys the stored credentials rather than only forgetting where they live, and logging in scrubs the old plaintext keys from an upgrading admin's existing cookie.
Fixed
-
The variant-playlist fetch is time-bounded again. The
.m3u8branch of the segment proxy calledhttpx.getwithouttimeout=_EMBY_HTTP_TIMEOUT, unlike the master-playlist and segment fetches either side of it. Every HLS request pulls a variant playlist, so this was the most-hit of the three upstream calls and the only unbounded one; a slow or misbehaving Emby could pin a uvicorn worker slot until the OS TCP timeout, which is the exact failure the constant exists to prevent. -
A failed session bind is no longer swallowed. Joining a party caught a failed cookie call and carried on, on the reasoning that the socket join carried the same identity. That held while
/hlsauthenticated on the URL token alone. It does not hold now: such a viewer would receive a stream URL and then 401 on every segment while chat, the participant list, and the member count kept working, so the party looked healthy and only the video was dead, with nothing logged and nothing shown. The bind now retries once to absorb a genuinely transient blip, then surfaces a banner with a working Retry that re-announces to the server and recovers playback without a page reload. -
Better behaviour on iPhone and iPad. The layout now honours the notch and home-indicator safe areas (
viewport-fit=coverplusenv(safe-area-inset-*)) and sizes against the dynamic viewport (100dvh), so controls no longer sit under Safari's collapsing toolbar. On the native-HLS path Safari uses, leaving a party now releases the stream instead of leaving the Emby transcode running, and playback blocked by the browser's autoplay policy is reported rather than failing silently. Lifted from #45. -
The Emby login modal traps focus. Tab and Shift+Tab cycle inside the dialog rather than escaping to the page behind it, Escape cancels, focus lands on the username field on open, and returns to wherever it was when the modal closes. Also from #45.
Added
- A tab tells you when another tab takes over the party. The session cookie holds exactly one party id and cookies are shared across every tab in a browser profile, so a second tab joining a different party silently repoints it and the first tab's playback stops. Each tab now announces its party over a
BroadcastChanneland a superseded tab says so, naming the other party, rather than stalling silently. Two tabs on the same party stay quiet, since both point the cookie at the same place. The banner leads with the no-action path (switch to the other tab) and puts the consequence in the button itself, because resuming here stops the other tab in turn: only one party can hold the cookie at a time. - Test coverage for both gaps (
tests/test_admin_session.py, plus expansion oftests/test_hls_proxy.pyto 8 tests). The admin tests decode theSet-Cookieheader exactly the way an attacker would and assert the token never appears in it. The HLS tests cover a missing cookie, a cleared host token, and a cookie/token party mismatch, the first automated coverage of the 423 the docstring has claimed since it was written. Both guards were checked for vacuousness by reintroducing the original bugs and confirming the suite fails.
Known limitation
Two different parties open in two tabs of the same browser profile now break the older tab's playback immediately, where previously the video kept playing. This is inherent to the session cookie holding a single party id; /api/image and /api/subtitles have degraded this way since 2.0.0, and this makes it total and visible rather than partial and silent. Separate browsers, separate profiles, incognito windows, and separate devices are all unaffected. Scoping session state per party would fix it properly and is deliberately left for a future release.
Installation
Option 1: Docker (Recommended)
Version specific:
docker pull ghcr.io/oratorian/emby-watchparty:2.1.0Or always latest stable version:
docker pull ghcr.io/oratorian/emby-watchparty:latestOr use docker-compose with the provided docker-compose.yml.example
Option 2: Manual Installation
- Download and extract
emby-watchparty-2.1.0.zip - Copy
.env.exampleto.envand configure your Emby server details - Install dependencies:
pip install -r requirements.txt - Run:
python app.py - Open browser to
http://localhost:5000
What's Included
- Complete application source code
- Docker support with pre-built images
- Requirements file for easy dependency installation
- Example configuration file
- README with full documentation
- MIT License
Requirements
- Python 3.8 or higher (manual install)
- Docker (container install)
- Emby server with API key
- Modern web browser
See README.md for full documentation.