Skip to content

v2.1.0

Choose a tag to compare

@github-actions github-actions released this 02 Aug 21:46
· 36 commits to main since this release
v2.1.0
83a77a4

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, the Referer header, reverse-proxy access logs, and copy-as-cURL. They are now gated by require_host_token, the same gate /api/image and /api/subtitles have used since 2.0.0. This is what CHANGELOG 2.0.0's breaking-change note and hls.py's own module docstring have described all along; git log -S require_host_token -- backend/src/routers/hls.py returns 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_token resolves 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_id no longer confers host or admin rights. Host identity was established by matching client_id alone, but host_client_id is broadcast to every member in the host_changed event, and POST /api/party/<id>/join stores whatever client_id the 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/config with full read/write whenever the host's Emby account had IsAdministrator=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 by host_session_grant, a 256-bit secret minted server-side by set_host, written only to the real host's cookie, never broadcast, and compared with compare_digest. It is rotated on every promotion and cleared on clear_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 SessionMiddleware signs the cookie but does not encrypt it, so the payload is base64(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-side AdminSessionStore with only an opaque handle in the cookie, mirroring how host_access_token has always been kept server-side. Not XSS-reachable (the cookie is httponly); the realistic exposure was proxy and CDN logs that capture headers, infostealers scraping browser cookie jars, and plaintext on the wire wherever SESSION_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 .m3u8 branch of the segment proxy called httpx.get without timeout=_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 /hls authenticated 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=cover plus env(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 BroadcastChannel and 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 of tests/test_hls_proxy.py to 8 tests). The admin tests decode the Set-Cookie header 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.0

Or always latest stable version:

docker pull ghcr.io/oratorian/emby-watchparty:latest

Or use docker-compose with the provided docker-compose.yml.example

Option 2: Manual Installation

  1. Download and extract emby-watchparty-2.1.0.zip
  2. Copy .env.example to .env and configure your Emby server details
  3. Install dependencies: pip install -r requirements.txt
  4. Run: python app.py
  5. 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.