Jellyfin Server 12.2 - Élevée - À distance - IDOR - GET /Items/Suggestions omit-userId expose le chemin et les ID de la bibliothèque masquée
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · jellyfin-suggestions-idor
Jellyfin Server 12.2 - Jellyfin
Classe : IDOR. Portée : Distante (tout utilisateur authentifié non administrateur).
GET /Items/Suggestions n'est protégé que par [Authorize]. Lorsque userId est omis, SuggestionsController.GetSuggestions définit user = null et n'appelle jamais RequestHelpers.GetUserId. InternalItemsQuery(null) n'appelle pas SetUser. LibraryManager.GetItemsResult saute AddUserToQuery. new DtoOptions() inclut ItemFields.Path. GetBaseItemDtos(..., user: null) saute IsVisible.
GHSA-9x85 a fait en sorte que GetUserId (l'omission renvoie aux claims ; un GUID étranger donne 403). ItemsController.GetItems l'utilise toujours. Suggestions ne le faisait pas. L'ancien GET /Users/{userId}/Suggestions possède toujours un GUID de route, donc ce point d'entrée voisin passe bien par GetUserId.
Un compte restreint extrait les titres, identifiants, résumés, identifiants de fournisseur et le Path du système de fichiers pour des bibliothèques auxquelles il n'est pas assigné. Ces GUIDs fonctionnent sur GET /Videos/{id}/stream non authentifié.
| ID | pas encore de CVE |
| 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 (ca0f16eb7c195f720a1493ed469a27c4657db0c6), image jellyfin/jellyfin:12.2 |
| Auth | authentifié (tout non-administrateur, y compris un compte enfant / bibliothèque masquée) |
| Licence | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1:18790 uniquement |
Reste n°1 des restes de Jellyfin 12.2. Même version épinglée, autres packs : jellyfin-svg-gzip (reste GHSA-cf3c gzip data:image/png), jellyfin-playlist-itemids (reste GHSA-9x85 ItemIds + stream non authentifié), jellyfin-ffmpeg-colonslash (reste GHSA-wwwm guillemets d'URL .strm). Ce pack est celui qui extrait la bibliothèque masquée.
Tout utilisateur authentifié non administrateur, y compris un compte enfant / bibliothèque masquée / MaxParentalRating. Nécessite un jeton utilisateur normal. Pas d'administrateur, pas de clé API. Il ne change pas les mots de passe. Il ne crée pas d'administrateur. Il inventorie les bibliothèques auxquelles il n'est pas assigné, y compris le Path du système de fichiers, puis lit ces GUIDs.
Table des privilèges (exhaustive). Même niveau que Signal-Server Dropwizard admin : méthode + chemin, ce que cet appel accorde, reconnaissance vs intégrité vs disponibilité, prouvé en labo vs déduit, surface adjacente qui reste fermée.
| Axe | Méthode + chemin | Accorde | Provenance |
|---|---|---|---|
| Recon | GET /Items/Suggestions sans userId (optionnel limit / mediaType / type) | Liste récursive d'éléments non délimitée : Name, Id, Overview, IDs de fournisseur, chemin du système de fichiers, métadonnées d'image des bibliothèques non assignées à l'appelant, y compris les dossiers accordés uniquement à un utilisateur masqué. L'ACL de dossier, la classification parentale et les tags autorisés ne sont pas appliqués. | Prouvé en labo. labuser restreint (EnableAllFolders=false, EnabledFolders=PublicB uniquement) : GET /Items témoin=0 ; Suggestions témoin=1 |
| Recon | Idem avec limit omis | InternalItemsQuery.Limit reste non défini ; le dépôt renvoie l'ensemble complet des correspondances (table entière des éléments, ordre aléatoire) | Source (SuggestionsController.cs:70-95) |
| Recon | GET /Videos/{itemId}/stream non authentifié avec un GUID issu de cette liste | Octets de ce fichier en tant qu'UID jellyfin | Source (VideosController n'a pas de [Authorize] au niveau de la classe ; StreamingHelpers GetItemById sans utilisateur). Flux testé en labo dans le résidu frère jellyfin-playlist-itemids, pas dans ce pack |
| Intégrité | aucun sur Suggestions | Aucune écriture de mot de passe, politique, favori ou session | Fermé |
| Disponibilité | aucun sur Suggestions | Aucun DoS au-delà du coût de requête ordinaire | Fermé |
Reste fermé sur cette version épinglée
| Axe | Méthode + chemin | Pourquoi c'est fermé |
|---|---|---|
| Recon | GET /Items?userId= et GET /Items?parentId=<hiddenFolder> | Toujours GetUserId + IsVisible (ItemsController.cs:265, 342-350) |
| Recon | GET /Playlists/{id}/Items?userId= | GetUserId (PlaylistsController.cs:524). Classe GHSA-9x85, corrigée |
| Recon | GET /Items/Suggestions?userId=<other> | Un GUID étranger passe toujours par GetUserId et renvoie 403 pour un non-admin. Prouvé en labo (impersonate-403=yes) |
| Recon | GET /Users/{userId}/Suggestions hérité | Le GUID de route est requis, donc il appelle bien GetUserId |
| Intégrité | POST /Users/Password?userId= | AssertCanUpdateUser |
| Intégrité | POST /Users/{id}/Policy | RequiresElevation |
| Intégrité | appareils, sessions, AddAdditionalUser | élévation ou session propre uniquement |
| Recon | GET /Users / GET /Users/{id} | UserDto + politique (GUID de dossiers, bit admin), pas le contenu des bibliothèques ni les hashes. Énumération du foyer, pas ce résidu |
Je ne prétends pas à une RCE par DLL de plugin, un reverse shell ou un vol de mot de passe. Le dump est le catalogue (et le Path) des bibliothèques non assignées à l'appelant. Le suivi du flux par GUID est un comportement Jellyfin HTML5/DLNA de longue date ; ce résidu est ce qui énumère les IDs.
v12.2 a fermé la vague GHSA de 12.0. Je suis resté sur ce tag malgré tout et j'ai lu chaque requête userId à côté de RequestHelpers.GetUserId. Items l'utilise toujours. Suggestions bifurque : userId vide -> user = null, jamais GetUserId. C'est le résidu.
run.sh initialise deux bibliothèques de films (PrivateA nommée avec le témoin, PublicB), termine l'assistant, crée labuser avec EnableAllFolders=false et EnabledFolders=PublicB uniquement, puis compare GET /Items à GET /Items/Suggestions.
Fausses pistes déjà consignées : traiter GET /Items comme la fuite (il est délimité) ; traiter Suggestions?userId=<admin> comme la fuite (403) ; traiter la route héritée /Users/{userId}/Suggestions comme la faille (elle a un GUID donc elle appelle GetUserId) ; revendiquer le flux non authentifié comme bug de ce pack (résidu frère) ; un reverse shell. Du théâtre.