Jellyfin Server 12.2 — высокий уровень — удалённо — IDOR — оставшиеся ItemIds публичного плейлиста из GHSA-9x85, затем неаутентифицированный поток
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · jellyfin-playlist-itemids
Класс: IDOR Доступ: Удалённый (любой пользователь, который видит публичный плейлист)
Jellyfin Server v12.2 (ca0f16eb7c195f720a1493ed469a27c4657db0c6) - Jellyfin
GHSA-9x85 исправил GET /Playlists/{id}/Items с помощью GetUserId и Where(i => i.Item2.IsVisible(user)). Для GET /Playlists/{id} этот фильтр не был добавлен. ItemIds — это GetManageableItems() без IsVisible для дочерних элементов. GetPlaylistForUser требует лишь видимости плейлиста.
CreatePlaylistDto.IsPublic по умолчанию true. Плейлисты OpenAccess являются IsVisible для других пользователей (HasParentalAllowedChild всё равно позволяет второму пользователю видеть плейлист). У неаутентифицированного GET /Videos/{id}/stream нет [Authorize]. StreamingHelpers загружает GetItemById без пользователя.
Пользователь, который видит публичный плейлист, читает GUID приватных дочерних элементов из ItemIds, а затем стримит файлы без токена.
| ID | CVE пока нет |
| Класс | IDOR (остаток ItemIds плейлиста от GHSA-9x85, затем неаутентифицированный стрим) |
| Доступ | Удалённый (любой пользователь, который видит публичный плейлист) |
| CWE | CWE-639, CWE-862 |
| CVSS | High: 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 (ca0f16eb) образ jellyfin/jellyfin:12.2 |
| Аутентификация | аутентификация для GET плейлиста; отсутствует для стрима |
| Лицензия | GNU Affero GPL v3.0 |
| Лаборатория | только 127.0.0.1:18800 |
Остаток #3 из Jellyfin 12.2 leftovers. Остаток #1 (самый важный): jellyfin-suggestions-idor. Тот же пин: jellyfin-svg-gzip · jellyfin-ffmpeg-colonslash.
Жертва и атакующий — два пользователя на одном сервере с непересекающимися библиотеками (детские против взрослых или скрытая папка). Жертва добавляет приватный фильм в публичный плейлист. При создании через тело запроса IsPublic по умолчанию true, так что это обычный случай, а не шаринг. Атакующий входит под ограниченной учётной записью, читает объект плейлиста, а затем получает файл без заголовка Authorization.
Это GUID медиафайлов из библиотек, к которым атакующий не привязан, а затем полный файл через стрим. Это не перебор названий плейлистов. Добавление/создание по-прежнему отбрасывает невидимые элементы.
| Метод | Путь | Привилегия |
|---|---|---|
| GET | /Playlists/{playlistId} | GUID дочерних элементов видимого плейлиста (ItemIds из GetManageableItems(), без IsVisible для дочерних элементов). Также Shares[].UserId / CanEdit (userId получателей доступа). Лаборатория: GET /Playlists/{id}/Items пользователя B пуст; GET /Playlists/{id} с тем же id перечислил приватный GUID a2bf95146e44897be6fb09020a82eb52. |
GetPlaylistForUser требует лишь видимости самого плейлиста. Достаточно OpenAccess плюс HasParentalAllowedChild (который пропускает проверку разрешённых тегов). Исправленный маршрут Items служит контролем: он по-прежнему фильтрует.
| Метод | Путь | Привилегия |
|---|---|---|
| GET | /Videos/{itemId}/stream?static=true | Байты приватного медиа. Нет [Authorize]. StreamingHelpers выполняет GetItemById<BaseItem>(id) без пользователя. В лаборатории: неаутентифицированный запрос 200, 1142 байта, свидетель JELLYFIN-PLAYLIST-ITEMIDS-WITNESS в теле. |
| GET | /Audio/{itemId}/stream | Тот же приёмник для аудио. Тот же помощник, тот же неограниченный поиск. |
Аутентифицированный HLS GET /Videos/{itemId}/master.m3u8 использует тот же путь GetItemById (DynamicHlsController имеет [Authorize] на уровне класса). В лаборатории проверялся неаутентифицированный прогрессивный поток, а не HLS.
| Метод | Путь | Привилегия |
|---|---|---|
| POST | /Playlists/{id}/Items и создание Ids | Закрыто. Playlist.GetPlaylistItems выполняет IsVisible. Приватные GUID отбрасываются, а не связываются. |
GetItemById(id, user)).EnableCollectionManagement по умолчанию false; FilterLinkedChildrenPerUser).ValidateTranscodePath удерживает устаревшие HLS-сегменты внутри каталога транскодирования).userId в Items по GHSA-9x85 (GetUserId возвращает 403 для чужого id).GET /Playlists/{id}/Items (исправленный маршрут; в лаборатории items-filtered=yes).Никакой записи паролей/политик. Никакой установки плагинов. Никакого shell.
Один только ACL папки (EnabledFolders) не скрыл дочерний элемент на /Items в этой лаборатории. AllowedTags=["lab-public"] заставил IsVisible отбросить его. ItemIds и неаутентифицированный поток не обращаются к этому фильтру.
Я зафиксировал jellyfin/jellyfin v12.2 (ca0f16eb). Двадцать поисков по исходникам на остатках волны GHSA 12.0. GHSA-9x85 научил GetPlaylistItems фильтровать дочерние элементы. Я спросил, получил ли GetPlaylist тот же Where. Не получил. Эндпоинты потоковой передачи так и не обзавелись привязанным к пользователю GetItemById.
Неверный поворот: трактовка EnabledFolders как скрытия Items. IsVisible фильма — это родительский контроль/теги, а не ACL папки. В лаборатории пришлось задать AllowedTags, чтобы исправленный маршрут Items стал пустым. Это контроль. Остаток его не использует.
git clone https://github.com/abraxas/jellyfin-playlist-itemids
cd jellyfin-playlist-itemids
python3 jellyfin-playlist-itemids-Abraxas-Labs.py
Или:
cd lab
./run.sh
Цель — только http://127.0.0.1:18800. Образ jellyfin/jellyfin:12.2. run.sh создаёт два крошечных mp4 (PrivateA — файл-свидетель, PublicB — пустышка). Мастер создаёт labadmin, обе папки, затем labuser с EnableAllFolders=false, EnabledFolders=PublicB, AllowedTags=["lab-public"]. Администратор помещает приватный фильм в публичный плейлист. Loopback. Только свидетель. Требуются Docker и ffmpeg для создания файлов.
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