Jellyfin Server 12.2 – Hoch – Remote – IDOR – öffentliche Playlist ItemIds Überbleibsel von GHSA-9x85, dann unauthentifizierter Stream
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · jellyfin-playlist-itemids
Klasse: IDOR Reichweite: Remote (jeder Benutzer, der eine öffentliche Playlist sehen kann)
Jellyfin Server v12.2 (ca0f16eb7c195f720a1493ed469a27c4657db0c6) - Jellyfin
GHSA-9x85 hat GET /Playlists/{id}/Items mit GetUserId plus Where(i => i.Item2.IsVisible(user)) gepatcht. GET /Playlists/{id} erhielt diesen Filter nicht. ItemIds ist GetManageableItems() ohne IsVisible für die Kindelemente. GetPlaylistForUser erfordert nur die Sichtbarkeit der Playlist.
CreatePlaylistDto.IsPublic ist standardmäßig true. OpenAccess-Playlists sind für andere Benutzer IsVisible (HasParentalAllowedChild lässt einen zweiten Benutzer die Playlist weiterhin sehen). Unauthentifiziertes GET /Videos/{id}/stream hat kein [Authorize]. StreamingHelpers lädt GetItemById ohne Benutzer.
Ein Benutzer, der eine öffentliche Playlist sehen kann, liest private Kind-GUIDs aus ItemIds und streamt anschließend die Dateien ohne Token.
| ID | noch keine CVE |
| Klasse | IDOR (Playlist-ItemIds-Überbleibsel von GHSA-9x85, dann unauthentifizierter Stream) |
| Reichweite | Remote (jeder Benutzer, der eine öffentliche Playlist sehen kann) |
| CWE | CWE-639, CWE-862 |
| CVSS | High: 6.5 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N |
| Produkt | Jellyfin Server |
| Betroffen | v12.2 (ca0f16eb) Image jellyfin/jellyfin:12.2 |
| Auth | authentifiziert für das Playlist-GET; keinerlei für den Stream |
| Lizenz | GNU Affero GPL v3.0 |
| Lab | nur 127.0.0.1:18800 |
Überbleibsel #3 von Jellyfin 12.2 leftovers. Überbleibsel #1 (am wichtigsten): jellyfin-suggestions-idor. Gleicher Pin: jellyfin-svg-gzip · jellyfin-ffmpeg-colonslash.
Opfer und Angreifer sind zwei Benutzer auf einem Server mit disjunkten Bibliotheken (Kinder vs. Erwachsene oder ein versteckter Ordner). Das Opfer legt einen privaten Film auf eine öffentliche Playlist. Bei der Erstellung per Body ist IsPublic standardmäßig true, das ist also der Normalfall, keine Freigabe. Der Angreifer meldet sich mit einem eingeschränkten Konto an, liest das Playlist-Objekt und ruft anschließend die Datei ohne Authorization-Header ab.
Das sind GUIDs von Medien in Bibliotheken, denen der Angreifer nicht zugewiesen ist, und dann die vollständige Datei über den Stream. Es ist keine Enumeration von Playlist-Titeln. Hinzufügen/Erstellen verwerfen weiterhin nicht sichtbare Elemente.
| Methode | Pfad | Berechtigung |
|---|---|---|
| GET | /Playlists/{playlistId} | Kind-GUIDs einer sichtbaren Playlist (ItemIds aus GetManageableItems(), kein IsVisible für Kindelemente). Außerdem Shares[].UserId / CanEdit (Sharee-UserIds). Lab: GET /Playlists/{id}/Items von B leer; GET /Playlists/{id} derselben ID listete die private GUID a2bf95146e44897be6fb09020a82eb52. |
GetPlaylistForUser erfordert nur, dass die Playlist selbst sichtbar ist. OpenAccess plus HasParentalAllowedChild (was die Prüfung auf erlaubte Tags überspringt) reicht aus. Die gepatchte Items-Route ist die Kontrolle: Sie filtert weiterhin.
| Methode | Pfad | Berechtigung |
|---|---|---|
| GET | /Videos/{itemId}/stream?static=true | Private Medienbytes. Kein [Authorize]. StreamingHelpers führt GetItemById<BaseItem>(id) ohne Benutzer aus. Labor: unauth 200, 1142 Bytes, Zeuge JELLYFIN-PLAYLIST-ITEMIDS-WITNESS im Body. |
| GET | /Audio/{itemId}/stream | Derselbe Sink für Audio. Derselbe Helper, derselbe ungescopte Lookup. |
Authentifiziertes HLS GET /Videos/{itemId}/master.m3u8 ist derselbe GetItemById-Pfad (DynamicHlsController hat klassenweites [Authorize]). Das Labor hat den unauth Progressive Stream getestet, nicht HLS.
| Methode | Pfad | Berechtigung |
|---|---|---|
| POST | /Playlists/{id}/Items und Erstellen von Ids | Geschlossen. Playlist.GetPlaylistItems führt IsVisible aus. Private GUIDs werden verworfen, nicht verlinkt. |
GetItemById(id, user)).EnableCollectionManagement standardmäßig false; FilterLinkedChildrenPerUser).ValidateTranscodePath hält Legacy-HLS-Segmente innerhalb des Transcode-Verzeichnisses).userId-Impersonation bei Items (GetUserId liefert 403 bei einer fremden ID).GET /Playlists/{id}/Items-Auflistung privater Kinder (die gepatchte Route; Labor items-filtered=yes).Kein Passwort-/Policy-Schreiben. Keine Plugin-Installation. Keine Shell.
Ordner-ACL (EnabledFolders) allein hat das Kind auf /Items in diesem Labor nicht verborgen. AllowedTags=["lab-public"] ließ IsVisible es verwerfen. ItemIds und der unauth Stream konsultieren diesen Filter nicht.
Ich habe jellyfin/jellyfin v12.2 (ca0f16eb) gepinnt. Zwanzig Quellcode-Suchen nach Überbleibseln der 12.0-GHSA-Welle. GHSA-9x85 brachte GetPlaylistItems dazu, Kinder zu filtern. Ich fragte, ob GetPlaylist dasselbe Where bekam. Das war nicht der Fall. Stream-Endpunkte erhielten nie ein benutzerscoped GetItemById.
Fehlweg: EnabledFolders als das Verbergen der Items zu behandeln. Movie-IsVisible ist parental/tags, nicht Ordner-ACL. Das Labor musste AllowedTags setzen, damit die gepatchte Items-Route leer blieb. Das ist die Kontrolle. Das Überbleibsel nutzt sie nicht.
git clone https://github.com/abraxas/jellyfin-playlist-itemids
cd jellyfin-playlist-itemids
python3 jellyfin-playlist-itemids-Abraxas-Labs.py
Oder:
cd lab
./run.sh
Ziel ist ausschließlich http://127.0.0.1:18800. Image jellyfin/jellyfin:12.2. run.sh legt zwei winzige mp4s an (PrivateA als Zeugen-Dateiname, PublicB als Dummy). Der Assistent erstellt labadmin, beide Ordner, dann labuser mit EnableAllFolders=false, EnabledFolders=PublicB, AllowedTags=["lab-public"]. Der Administrator legt den privaten Film auf eine öffentliche Playlist. Loopback. Nur zur Zeugenerfassung. Erfordert Docker und ffmpeg zum Anlegen.
SUCCESS JELLYFIN-PLAYLIST-ITEMIDS items-filtered=yes itemids-leak=yes unauth-stream=200 openAccess=yes playlist=e4798134497195ff4733dde62428e8d8 private=a2bf95146e44897be6fb09020a82eb52 witness-in-stream=yes witness-in-playlist-json=yes JELLYFIN-PLAYLIST-ITEMIDS-WITNESS
Replay: python3 jellyfin-playlist-itemids-Abraxas-Labs.py.