Jellyfin Server 12.2 - Alta - Remoto - IDOR - ItemIds residui della playlist pubblica di GHSA-9x85 poi streaming non autenticato
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · jellyfin-playlist-itemids
Classe: IDOR Portata: Remota (qualsiasi utente che può vedere una playlist pubblica)
Jellyfin Server v12.2 (ca0f16eb7c195f720a1493ed469a27c4657db0c6) - Jellyfin
GHSA-9x85 ha corretto GET /Playlists/{id}/Items con GetUserId più Where(i => i.Item2.IsVisible(user)). A GET /Playlists/{id} non è stato applicato quel filtro. ItemIds è GetManageableItems() senza IsVisible sui figli. GetPlaylistForUser richiede solo la visibilità della playlist.
CreatePlaylistDto.IsPublic è true per impostazione predefinita. Le playlist OpenAccess sono IsVisible per gli altri utenti (HasParentalAllowedChild consente comunque a un secondo utente di vedere la playlist). GET /Videos/{id}/stream non autenticato non ha [Authorize]. StreamingHelpers carica GetItemById senza un utente.
Un utente che può vedere una playlist pubblica legge i GUID privati dei figli da ItemIds, poi trasmette i file senza token.
| ID | nessuna CVE al momento |
| Classe | IDOR (residuo di ItemIds della playlist di GHSA-9x85, poi stream non autenticato) |
| Portata | Remota (qualsiasi utente che può vedere una playlist pubblica) |
| CWE | CWE-639, CWE-862 |
| CVSS | Alto: 6.5 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N |
| Prodotto | Jellyfin Server |
| Affetto | v12.2 (ca0f16eb) immagine jellyfin/jellyfin:12.2 |
| Auth | autenticato per la GET della playlist; nessuno per lo stream |
| Licenza | GNU Affero GPL v3.0 |
| Lab | solo 127.0.0.1:18800 |
Residuo #3 di Jellyfin 12.2 leftovers. Residuo #1 (il più importante): jellyfin-suggestions-idor. Stesso pin: jellyfin-svg-gzip · jellyfin-ffmpeg-colonslash.
Vittima e attaccante sono due utenti sullo stesso server con librerie disgiunte (bambini vs adulti, o una cartella nascosta). La vittima inserisce un film privato in una playlist pubblica. IsPublic predefinito a true nella creazione del body, quindi è il caso usuale, non una condivisione. L'attaccante accede come account limitato, legge l'oggetto playlist, poi recupera il file senza header Authorization.
Si tratta dei GUID dei media in librerie a cui l'attaccante non è assegnato, poi del file completo tramite stream. Non è un'enumerazione dei titoli delle playlist. Aggiunta/creazione scartano comunque gli elementi non visibili.
| Metodo | Percorso | Privilegio |
|---|---|---|
| GET | /Playlists/{playlistId} | GUID dei figli di una playlist visibile (ItemIds da GetManageableItems(), nessun IsVisible sui figli). Anche Shares[].UserId / CanEdit (userId dei destinatari della condivisione). Lab: GET /Playlists/{id}/Items di B vuota; GET /Playlists/{id} dello stesso id elencava il GUID privato a2bf95146e44897be6fb09020a82eb52. |
GetPlaylistForUser richiede solo che la playlist stessa sia visibile. OpenAccess più HasParentalAllowedChild (che salta il controllo dei tag consentiti) è sufficiente. La route Items corretta è il controllo: filtra ancora.
| Metodo | Percorso | Privilegio |
|---|---|---|
| GET | /Videos/{itemId}/stream?static=true | Byte di media privati. Nessun [Authorize]. StreamingHelpers esegue GetItemById<BaseItem>(id) senza un utente. Lab: 200 non autenticato, 1142 byte, witness JELLYFIN-PLAYLIST-ITEMIDS-WITNESS nel body. |
| GET | /Audio/{itemId}/stream | Stessa sink per l'audio. Stesso helper, stessa lookup non limitata. |
L'HLS autenticato GET /Videos/{itemId}/master.m3u8 è lo stesso percorso GetItemById (DynamicHlsController ha [Authorize] a livello di classe). Il lab ha esercitato lo stream progressivo non autenticato, non HLS.
| Metodo | Percorso | Privilegio |
|---|---|---|
| POST | /Playlists/{id}/Items e creazione Ids | Chiusa. Playlist.GetPlaylistItems esegue IsVisible. I GUID privati vengono scartati, non collegati. |
GetItemById(id, user) con scope utente).EnableCollectionManagement predefinito false; FilterLinkedChildrenPerUser).ValidateTranscodePath mantiene i segmenti HLS legacy all'interno della directory transcode).userId GHSA-9x85 su Items (GetUserId restituisce 403 per un id estraneo).GET /Playlists/{id}/Items dei figli privati (la route corretta; lab items-filtered=yes).Nessuna scrittura di password/policy. Nessuna installazione di plugin. Nessuna shell.
La sola ACL delle cartelle (EnabledFolders) non ha nascosto il figlio su /Items in questo lab. AllowedTags=["lab-public"] ha fatto sì che IsVisible lo scartasse. ItemIds e lo stream non autenticato non consultano quel filtro.
Ho fissato jellyfin/jellyfin v12.2 (ca0f16eb). Venti ricerche nel codice sorgente sui residui dell'ondata GHSA 12.0. GHSA-9x85 ha insegnato a GetPlaylistItems a filtrare i figli. Mi sono chiesto se GetPlaylist avesse ricevuto lo stesso Where. Non l'ha ricevuto. Gli endpoint di streaming non hanno mai ricevuto un GetItemById con scope utente.
Falsa pista: trattare EnabledFolders come l'occultamento di Items. IsVisible di un film è basato su parental/tag, non sull'ACL delle cartelle. Il lab ha dovuto impostare AllowedTags affinché la route Items corretta risultasse vuota. Questo è il controllo. Il residuo non lo usa.
git clone https://github.com/abraxas/jellyfin-playlist-itemids
cd jellyfin-playlist-itemids
python3 jellyfin-playlist-itemids-Abraxas-Labs.py
O:
cd lab
./run.sh
Prendi di mira solo http://127.0.0.1:18800. Immagine jellyfin/jellyfin:12.2. run.sh prepara due minuscoli mp4 (PrivateA con nome file testimone, PublicB fittizio). La procedura guidata crea labadmin, entrambe le cartelle, poi labuser con EnableAllFolders=false, EnabledFolders=PublicB, AllowedTags=["lab-public"]. L'amministratore inserisce il film privato in una playlist pubblica. Loopback. Solo testimone. Richiede Docker e ffmpeg per il seeding.
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.