Jellyfin Server 12.2 - Élevée - Distant - IDOR - ItemIds de playlist publique restants de GHSA-9x85 puis diffusion non authentifiée
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · jellyfin-playlist-itemids
Classe : IDOR Portée : Distante (tout utilisateur pouvant voir une playlist publique)
Jellyfin Server v12.2 (ca0f16eb7c195f720a1493ed469a27c4657db0c6) - Jellyfin
GHSA-9x85 a corrigé GET /Playlists/{id}/Items avec GetUserId plus Where(i => i.Item2.IsVisible(user)). GET /Playlists/{id} n'a pas reçu ce filtre. ItemIds correspond à GetManageableItems() sans IsVisible sur les enfants. GetPlaylistForUser exige seulement que la playlist soit visible.
CreatePlaylistDto.IsPublic vaut true par défaut. Les playlists OpenAccess sont IsVisible pour les autres utilisateurs (HasParentalAllowedChild permet toujours à un second utilisateur de voir la playlist). GET /Videos/{id}/stream sans authentification n'a pas de [Authorize]. StreamingHelpers charge GetItemById sans utilisateur.
Un utilisateur qui peut voir une playlist publique lit les GUID des enfants privés depuis ItemIds, puis diffuse les fichiers sans jeton.
| ID | pas encore de CVE |
| Classe | IDOR (ItemIds de playlist, résidu de GHSA-9x85, puis diffusion sans authentification) |
| Portée | Distante (tout utilisateur pouvant voir une playlist publique) |
| CWE | CWE-639, CWE-862 |
| CVSS | Élevé : 6.5 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N |
| Produit | Jellyfin Server |
| Affecté | v12.2 (ca0f16eb) image jellyfin/jellyfin:12.2 |
| Auth | authentifié pour le GET de la playlist ; aucun pour le flux |
| Licence | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1:18800 uniquement |
Résidu #3 de Jellyfin 12.2 leftovers. Résidu #1 (le plus important) : jellyfin-suggestions-idor. Même version épinglée : jellyfin-svg-gzip · jellyfin-ffmpeg-colonslash.
La victime et l'attaquant sont deux utilisateurs sur un même serveur avec des bibliothèques disjointes (enfants vs adultes, ou un dossier masqué). La victime place un film privé sur une playlist publique. La création par corps avec IsPublic vaut true par défaut, c'est donc le cas habituel, pas un partage. L'attaquant se connecte avec un compte restreint, lit l'objet playlist, puis récupère le fichier sans en-tête Authorization.
Il s'agit des GUID de médias dans des bibliothèques non attribuées à l'attaquant, puis du fichier complet via le flux. Ce n'est pas de l'énumération de titres de playlists. L'ajout/création écarte toujours les éléments non visibles.
| Méthode | Chemin | Privilège |
|---|---|---|
| GET | /Playlists/{playlistId} | GUID des enfants d'une playlist visible (ItemIds issu de GetManageableItems(), sans IsVisible sur les enfants). Aussi Shares[].UserId / CanEdit (userId des destinataires du partage). Lab : le GET /Playlists/{id}/Items de B est vide ; le GET /Playlists/{id} du même id liste le GUID privé a2bf95146e44897be6fb09020a82eb52. |
GetPlaylistForUser exige seulement que la playlist elle-même soit visible. OpenAccess plus HasParentalAllowedChild (qui contourne la vérification des tags autorisés) suffit. La route Items corrigée sert de contrôle : elle filtre toujours.
| Méthode | Chemin | Privilège |
|---|---|---|
| GET | /Videos/{itemId}/stream?static=true | Octets de média privé. Pas de [Authorize]. StreamingHelpers fait GetItemById<BaseItem>(id) sans utilisateur. Lab : 200 sans auth, 1142 octets, témoin JELLYFIN-PLAYLIST-ITEMIDS-WITNESS dans le corps. |
| GET | /Audio/{itemId}/stream | Même puits pour l'audio. Même helper, même recherche non délimitée. |
Le HLS authentifié GET /Videos/{itemId}/master.m3u8 emprunte le même chemin GetItemById (DynamicHlsController a [Authorize] au niveau de la classe). Le lab a exercé le flux progressif non authentifié, pas le HLS.
| Méthode | Chemin | Privilège |
|---|---|---|
| POST | /Playlists/{id}/Items et création de Ids | Fermé. Playlist.GetPlaylistItems exécute IsVisible. Les GUID privés sont écartés, pas liés. |
GetItemById(id, user) limité à l'utilisateur).EnableCollectionManagement par défaut à false ; FilterLinkedChildrenPerUser).ValidateTranscodePath maintient les segments HLS hérités à l'intérieur du répertoire de transcodage).userId GHSA-9x85 sur Items (GetUserId renvoie 403 pour un id étranger).GET /Playlists/{id}/Items des enfants privés (la route corrigée ; lab items-filtered=yes).Aucune écriture de mot de passe/politique. Aucune installation de plugin. Aucun shell.
L'ACL de dossier (EnabledFolders) seule n'a pas masqué l'enfant sur /Items dans ce lab. AllowedTags=["lab-public"] a fait que IsVisible l'écarte. ItemIds et le flux non authentifié ne consultent pas ce filtre.
J'ai épinglé jellyfin/jellyfin v12.2 (ca0f16eb). Vingt recherches dans le code source sur les résidus de la vague GHSA 12.0. GHSA-9x85 a appris à GetPlaylistItems à filtrer les enfants. Je me suis demandé si GetPlaylist avait reçu le même Where. Ce n'était pas le cas. Les endpoints de flux n'ont jamais reçu de GetItemById limité à l'utilisateur.
Fausse piste : traiter EnabledFolders comme le masquage des Items. IsVisible d'un film relève du contrôle parental/des tags, pas de l'ACL de dossier. Le lab a dû définir AllowedTags pour que la route Items corrigée devienne vide. C'est le contrôle. Le résidu ne l'utilise pas.
git clone https://github.com/abraxas/jellyfin-playlist-itemids
cd jellyfin-playlist-itemids
python3 jellyfin-playlist-itemids-Abraxas-Labs.py
Ou :
cd lab
./run.sh
Cibler uniquement http://127.0.0.1:18800. Image jellyfin/jellyfin:12.2. run.sh initialise deux minuscules mp4 (PrivateA comme nom de fichier témoin, PublicB factice). L'assistant crée labadmin, les deux dossiers, puis labuser avec EnableAllFolders=false, EnabledFolders=PublicB, AllowedTags=["lab-public"]. L'administrateur place le film privé dans une playlist publique. Loopback. Témoin uniquement. Nécessite Docker et ffmpeg pour l'initialisation.
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