Jellyfin Server 12.2 - Alta - Remota - IDOR - ItemIds de listas de reproducción públicas remanentes de GHSA-9x85, luego transmisión no autenticada
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · jellyfin-playlist-itemids
Clase: IDOR Alcance: Remoto (cualquier usuario que pueda ver una lista de reproducción pública)
Jellyfin Server v12.2 (ca0f16eb7c195f720a1493ed469a27c4657db0c6) - Jellyfin
GHSA-9x85 parcheó GET /Playlists/{id}/Items con GetUserId más Where(i => i.Item2.IsVisible(user)). A GET /Playlists/{id} no se le aplicó ese filtro. ItemIds es GetManageableItems() sin IsVisible en los hijos. GetPlaylistForUser solo requiere que la lista de reproducción sea visible.
CreatePlaylistDto.IsPublic tiene como valor predeterminado true. Las listas de reproducción OpenAccess son IsVisible para otros usuarios (HasParentalAllowedChild todavía permite que un segundo usuario vea la lista de reproducción). GET /Videos/{id}/stream sin autenticación no tiene [Authorize]. StreamingHelpers carga GetItemById sin un usuario.
Un usuario que puede ver una lista de reproducción pública lee los GUID de hijos privados desde ItemIds y luego transmite los archivos sin token.
| ID | aún sin CVE |
| Clase | IDOR (resto de ItemIds de la lista de reproducción de GHSA-9x85, luego stream sin autenticación) |
| Alcance | Remoto (cualquier usuario que pueda ver una lista de reproducción pública) |
| 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 |
| Producto | Jellyfin Server |
| Afectado | v12.2 (ca0f16eb) imagen jellyfin/jellyfin:12.2 |
| Autenticación | autenticado para el GET de la lista de reproducción; ninguno para el stream |
| Licencia | GNU Affero GPL v3.0 |
| Laboratorio | solo 127.0.0.1:18800 |
Resto #3 de Jellyfin 12.2 leftovers. Resto #1 (el más importante): jellyfin-suggestions-idor. Misma versión fijada: jellyfin-svg-gzip · jellyfin-ffmpeg-colonslash.
La víctima y el atacante son dos usuarios en un mismo servidor con bibliotecas disjuntas (niños vs adultos, o una carpeta oculta). La víctima coloca una película privada en una lista de reproducción pública. IsPublic en la creación del cuerpo tiene como valor predeterminado true, así que ese es el caso habitual, no un recurso compartido. El atacante inicia sesión como una cuenta restringida, lee el objeto de la lista de reproducción y luego obtiene el archivo sin cabecera Authorization.
Se trata de GUID de medios en bibliotecas a las que el atacante no está asignado, y luego el archivo completo mediante stream. No es enumeración de títulos de listas de reproducción. Añadir/crear todavía descarta los elementos no visibles.
| Método | Ruta | Privilegio |
|---|---|---|
| GET | /Playlists/{playlistId} | GUID de hijos de una lista de reproducción visible (ItemIds de GetManageableItems(), sin IsVisible en los hijos). También Shares[].UserId / CanEdit (userId de los destinatarios del recurso compartido). Laboratorio: el GET /Playlists/{id}/Items de B vacío; el GET /Playlists/{id} del mismo id listó el GUID privado a2bf95146e44897be6fb09020a82eb52. |
GetPlaylistForUser solo requiere que la propia lista de reproducción sea visible. OpenAccess más HasParentalAllowedChild (que omite la comprobación de etiquetas permitidas) es suficiente. La ruta Items parcheada es el control: sigue filtrando.
| Método | Ruta | Privilegio |
|---|---|---|
| GET | /Videos/{itemId}/stream?static=true | Bytes de medios privados. Sin [Authorize]. StreamingHelpers hace GetItemById<BaseItem>(id) sin un usuario. Lab: 200 sin autenticación, 1142 bytes, testigo JELLYFIN-PLAYLIST-ITEMIDS-WITNESS en el cuerpo. |
| GET | /Audio/{itemId}/stream | Mismo sink para audio. Mismo helper, misma búsqueda sin alcance. |
El HLS autenticado GET /Videos/{itemId}/master.m3u8 es la misma ruta GetItemById (DynamicHlsController tiene [Authorize] a nivel de clase). El lab ejercitó el stream progresivo sin autenticación, no HLS.
| Método | Ruta | Privilegio |
|---|---|---|
| POST | /Playlists/{id}/Items y crear Ids | Cerrado. Playlist.GetPlaylistItems ejecuta IsVisible. Los GUID privados se descartan, no se enlazan. |
GetItemById(id, user) con alcance de usuario).EnableCollectionManagement por defecto false; FilterLinkedChildrenPerUser).ValidateTranscodePath mantiene los segmentos HLS heredados dentro del directorio de transcodificación).userId de GHSA-9x85 en Items (GetUserId devuelve 403 para un id ajeno).GET /Playlists/{id}/Items (la ruta parcheada; lab items-filtered=yes).Sin escritura de contraseña/política. Sin instalación de plugins. Sin shell.
La ACL de carpeta (EnabledFolders) por sí sola no ocultó el hijo en /Items en este lab. AllowedTags=["lab-public"] hizo que IsVisible lo descartara. ItemIds y el stream sin autenticación no consultan ese filtro.
Fijé jellyfin/jellyfin v12.2 (ca0f16eb). Veinte búsquedas en el código fuente sobre los restos de la oleada de GHSA de la 12.0. GHSA-9x85 enseñó a GetPlaylistItems a filtrar hijos. Pregunté si GetPlaylist recibió el mismo Where. No lo hizo. Los endpoints de stream nunca recibieron un GetItemById con alcance de usuario.
Falso camino: tratar EnabledFolders como el ocultamiento de Items. IsVisible de Movie es parental/etiquetas, no ACL de carpeta. El lab tuvo que establecer AllowedTags para que la ruta parcheada de Items quedara vacía. Ese es el control. El resto no 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
Apunta solo a http://127.0.0.1:18800. Imagen jellyfin/jellyfin:12.2. run.sh siembra dos mp4 diminutos (nombre de archivo testigo PrivateA, dummy PublicB). El asistente crea labadmin, ambas carpetas, luego labuser con EnableAllFolders=false, EnabledFolders=PublicB, AllowedTags=["lab-public"]. El administrador coloca la película privada en una lista de reproducción pública. Loopback. Solo testigo. Requiere Docker y ffmpeg para sembrar.