Jellyfin Server 12.2 — высокий уровень — удалённо — IDOR — GET /Items/Suggestions omit-userId раскрывает скрытый путь библиотеки и идентификаторы
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · jellyfin-suggestions-idor
Jellyfin Server 12.2 - Jellyfin
Класс: IDOR. Доступ: удалённый (любой аутентифицированный пользователь без прав администратора).
GET /Items/Suggestions защищён только атрибутом [Authorize]. Когда userId опущен, SuggestionsController.GetSuggestions устанавливает user = null и никогда не вызывает RequestHelpers.GetUserId. InternalItemsQuery(null) не вызывает SetUser. LibraryManager.GetItemsResult пропускает AddUserToQuery. new DtoOptions() включает ItemFields.Path. GetBaseItemDtos(..., user: null) пропускает IsVisible.
GHSA-9x85 научил GetUserId (при пропуске берутся claims; чужой GUID даёт 403). ItemsController.GetItems всегда его использует. Suggestions — нет. Устаревший GET /Users/{userId}/Suggestions всегда имеет GUID в маршруте, поэтому этот родственный эндпоинт действительно проходит через GetUserId.
Аккаунт с ограничениями выгружает названия, ID, описания, идентификаторы провайдеров и путь в файловой системе для библиотек, к которым он не приписан. Эти GUID воспроизводятся на неаутентифицированном GET /Videos/{id}/stream.
| ID | CVE пока нет |
| CWE | CWE-639 / CWE-862 |
| CVSS | Высокий: 6.5 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N |
| Продукт | Jellyfin Server |
| Затронуто | v12.2 (ca0f16eb7c195f720a1493ed469a27c4657db0c6), образ jellyfin/jellyfin:12.2 |
| Аутентификация | аутентифицированный (любой не-администратор, включая детский аккаунт / аккаунт со скрытой библиотекой) |
| Лицензия | GNU Affero GPL v3.0 |
| Лаборатория | только 127.0.0.1:18790 |
Остаток №1 из Jellyfin 12.2 leftovers. Тот же пин, другие пакеты: jellyfin-svg-gzip (остаток GHSA-cf3c gzip data:image/png), jellyfin-playlist-itemids (остаток GHSA-9x85 ItemIds + неаутентифицированный поток), jellyfin-ffmpeg-colonslash (остаток GHSA-wwwm кавычки URL .strm). Этот пакет — тот, что выгружает скрытую библиотеку.
Любой аутентифицированный не-администратор, включая детский аккаунт / аккаунт со скрытой библиотекой / аккаунт с MaxParentalRating. Нужен обычный пользовательский токен. Без администратора, без API-ключа. Пароли они не меняют. Администратора не создают. Они инвентаризируют библиотеки, к которым не приписаны, включая путь в файловой системе, а затем воспроизводят эти GUID.
Таблица привилегий (исчерпывающая). Тот же уровень, что и Signal-Server Dropwizard admin: метод + путь, что даёт этот вызов, разведка против целостности против доступности, доказано в лаборатории против выведено, смежная поверхность, которая остаётся закрытой.
| Ось | Метод + путь | Что даёт | Происхождение |
|---|---|---|---|
| Разведка | GET /Items/Suggestions без userId (необязательные limit / mediaType / type) | Неограниченный рекурсивный список элементов: Name, Id, Overview, идентификаторы провайдеров, путь в файловой системе, метаданные изображений библиотек, к которым вызывающий не назначен, включая папки, выданные только скрытому пользователю. ACL папок, родительский рейтинг и разрешённые теги не применяются. | Доказано в лаборатории. Ограниченный labuser (EnableAllFolders=false, только EnabledFolders=PublicB): GET /Items witness=0; Suggestions witness=1 |
| Разведка | То же без limit | InternalItemsQuery.Limit остаётся неустановленным; репозиторий возвращает весь набор совпадений (вся таблица элементов, случайный порядок) | Исходный код (SuggestionsController.cs:70-95) |
| Разведка | неаутентифицированный GET /Videos/{itemId}/stream с GUID из этого списка | Байты этого файла от имени UID jellyfin | Исходный код (у VideosController нет атрибута [Authorize] на классе; StreamingHelpers GetItemById без пользователя). Поток проверен в лаборатории в соседнем остатке jellyfin-playlist-itemids, не в этом пакете |
| Целостность | нет в Suggestions | Нет записи пароля, политики, избранного или сессии | Закрыто |
| Доступность | нет в Suggestions | Нет DoS сверх обычной стоимости запроса | Закрыто |
Остаётся закрытым на этой фиксации
| Ось | Метод + путь | Почему закрыто |
|---|---|---|
| Разведка | GET /Items?userId= и GET /Items?parentId=<hiddenFolder> | Всегда GetUserId + IsVisible (ItemsController.cs:265, 342-350) |
| Разведка | GET /Playlists/{id}/Items?userId= | GetUserId (PlaylistsController.cs:524). Класс GHSA-9x85, исправлен |
| Разведка | GET /Items/Suggestions?userId=<other> | Чужой GUID всё равно попадает в GetUserId и возвращает 403 для не-администратора. Доказано в лаборатории (impersonate-403=yes) |
| Разведка | устаревший GET /Users/{userId}/Suggestions | GUID маршрута обязателен, поэтому он действительно вызывает GetUserId |
| Целостность | POST /Users/Password?userId= | AssertCanUpdateUser |
| Целостность | POST /Users/{id}/Policy | RequiresElevation |
| Целостность | устройства, сессии, AddAdditionalUser | только повышение прав или собственная сессия |
| Разведка | GET /Users / GET /Users/{id} | UserDto + политика (GUID папок, бит администратора), а не содержимое библиотек или хэши. Перечисление домашних пользователей, не этот остаток |
Я не заявляю о RCE через DLL плагина, обратной оболочке или краже паролей. Дамп — это каталог (и Path) библиотек, к которым вызывающий не был назначен. Продолжение в виде потока по GUID — давнее поведение Jellyfin HTML5/DLNA; этот остаток — то, что перечисляет ID.
v12.2 закрыла волну GHSA 12.0. Я всё равно остался на этом теге и прочитал каждый запрос userId рядом с RequestHelpers.GetUserId. Items всегда его использует. Suggestions ветвится: пустой userId -> user = null, никогда не GetUserId. Вот это и есть остаток.
run.sh создаёт две библиотеки фильмов (PrivateA с именем-свидетелем, PublicB), завершает мастер настройки, создаёт labuser с EnableAllFolders=false и только EnabledFolders=PublicB, затем сравнивает GET /Items с GET /Items/Suggestions.
Уже зафиксированные неверные пути: считать GET /Items утечкой (он ограничен областью); считать Suggestions?userId=<admin> утечкой (403); считать устаревший маршрут /Users/{userId}/Suggestions дырой (у него есть GUID, поэтому он вызывает GetUserId); заявлять неаутентифицированный поток как баг этого пакета (соседний остаток); обратная оболочка. Театр.