Jellyfin Server 12.2 - Alta - Remoto - IDOR - GET /Items/Suggestions omit-userId expõe o caminho e os IDs da biblioteca oculta
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · jellyfin-suggestions-idor
Jellyfin Server 12.2 - Jellyfin
Classe: IDOR. Alcance: Remoto (qualquer usuário autenticado não-admin).
GET /Items/Suggestions possui apenas [Authorize]. Quando userId é omitido, SuggestionsController.GetSuggestions define user = null e nunca chama RequestHelpers.GetUserId. InternalItemsQuery(null) não chama SetUser. LibraryManager.GetItemsResult ignora AddUserToQuery. new DtoOptions() inclui ItemFields.Path. GetBaseItemDtos(..., user: null) ignora IsVisible.
GHSA-9x85 ensinou GetUserId (omitir vai para as claims; um GUID estrangeiro resulta em 403). ItemsController.GetItems sempre o utiliza. Suggestions não o fazia. O legado GET /Users/{userId}/Suggestions sempre tem um GUID na rota, então esse irmão de fato passa por GetUserId.
Uma conta restrita extrai títulos, IDs, visões gerais, IDs de provedor e Path do sistema de arquivos de bibliotecas às quais não está atribuída. Esses GUIDs funcionam em GET /Videos/{id}/stream sem autenticação.
| ID | sem CVE ainda |
| 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 |
| Produto | Jellyfin Server |
| Afetado | v12.2 (ca0f16eb7c195f720a1493ed469a27c4657db0c6), imagem jellyfin/jellyfin:12.2 |
| Autenticação | autenticado (qualquer não-admin, incluindo uma conta infantil / de biblioteca oculta) |
| Licença | GNU Affero GPL v3.0 |
| Lab | apenas 127.0.0.1:18790 |
Sobra #1 de Jellyfin 12.2 leftovers. Mesmo pin, outros pacotes: jellyfin-svg-gzip (sobra do GHSA-cf3c gzip data:image/png), jellyfin-playlist-itemids (sobra do GHSA-9x85 ItemIds + stream sem autenticação), jellyfin-ffmpeg-colonslash (sobra do GHSA-wwwm aspas de URL .strm). Este pacote é o que extrai a biblioteca oculta.
Qualquer usuário autenticado não-admin, incluindo uma conta infantil / de biblioteca oculta / MaxParentalRating. Precisa de um token de usuário normal. Sem admin, sem chave de API. Não alteram senhas. Não criam admin. Eles inventariam bibliotecas às quais não foram atribuídos, incluindo o Path do sistema de arquivos, e então reproduzem esses GUIDs.
Tabela de privilégios (exaustiva). Mesmo padrão do Signal-Server Dropwizard admin: método + caminho, o que essa chamada concede, reconhecimento vs integridade vs disponibilidade, comprovado em lab vs inferido, superfície adjacente que permanece fechada.
| Eixo | Método + caminho | Concede | Proveniência |
|---|---|---|---|
| Recon | GET /Items/Suggestions sem userId (opcional limit / mediaType / type) | Lista recursiva de itens sem escopo: Name, Id, Overview, IDs de provedor, Path do sistema de arquivos, metadados de imagem de bibliotecas às quais o chamador não está atribuído, incluindo pastas concedidas apenas a um usuário oculto. ACL de pasta, classificação parental e tags permitidas não são aplicadas. | Comprovado em laboratório. labuser restrito (EnableAllFolders=false, EnabledFolders=PublicB apenas): GET /Items witness=0; Suggestions witness=1 |
| Recon | O mesmo com limit omitido | InternalItemsQuery.Limit permanece não definido; o repositório retorna o conjunto completo de correspondências (tabela inteira de itens, ordem aleatória) | Código-fonte (SuggestionsController.cs:70-95) |
| Recon | GET /Videos/{itemId}/stream sem autenticação com um GUID dessa lista | Bytes desse arquivo como o UID jellyfin | Código-fonte (VideosController não tem [Authorize] na classe; StreamingHelpers GetItemById sem um usuário). Stream testado em laboratório no leftover irmão jellyfin-playlist-itemids, não neste pacote |
| Integridade | nenhum em Suggestions | Nenhuma escrita de senha, política, favorito ou sessão | Fechado |
| Disponibilidade | nenhum em Suggestions | Nenhum DoS além do custo normal de consulta | Fechado |
Permanece fechado neste pin
| Eixo | Método + caminho | Por que está fechado |
|---|---|---|
| Recon | GET /Items?userId= e GET /Items?parentId=<hiddenFolder> | Sempre GetUserId + IsVisible (ItemsController.cs:265, 342-350) |
| Recon | GET /Playlists/{id}/Items?userId= | GetUserId (PlaylistsController.cs:524). Classe GHSA-9x85, corrigida |
| Recon | GET /Items/Suggestions?userId=<other> | GUID estrangeiro ainda passa por GetUserId e retorna 403 para um não-admin. Comprovado em laboratório (impersonate-403=yes) |
| Recon | rota legada GET /Users/{userId}/Suggestions | O GUID da rota é obrigatório, então ela de fato chama GetUserId |
| Integridade | POST /Users/Password?userId= | AssertCanUpdateUser |
| Integridade | POST /Users/{id}/Policy | RequiresElevation |
| Integridade | dispositivos, sessões, AddAdditionalUser | elevação ou apenas a própria sessão |
| Recon | GET /Users / GET /Users/{id} | UserDto + política (GUIDs de pasta, bit de admin), não conteúdos de biblioteca ou hashes. Enumeração doméstica, não este leftover |
Não estou alegando RCE via DLL de plugin, reverse shell ou roubo de senha. O dump é o catálogo (e o Path) das bibliotecas às quais o chamador não foi atribuído. O follow-on de stream por GUID é um comportamento de longa data do HTML5/DLNA do Jellyfin; este leftover é o que enumera os IDs.
A v12.2 fechou a onda de GHSAs da 12.0. Mesmo assim, fiquei nessa tag e li cada consulta userId ao lado de RequestHelpers.GetUserId. Items sempre a usa. Suggestions ramifica: userId vazio -> user = null, nunca GetUserId. Esse é o leftover.
run.sh semeia duas bibliotecas de filmes (PrivateA nomeada com o witness, PublicB), conclui o assistente, cria labuser com EnableAllFolders=false e EnabledFolders=PublicB apenas, e então compara GET /Items com GET /Items/Suggestions.
Caminhos errados já registrados: tratar GET /Items como o vazamento (ele tem escopo); tratar Suggestions?userId=<admin> como o vazamento (403); tratar a rota legada /Users/{userId}/Suggestions como a falha (ela tem um GUID, então chama GetUserId); alegar o stream sem autenticação como bug deste pacote (leftover irmão); um reverse shell. Teatro.
Bugs de cliente parecem bugs de produto. Um helper que sombreia http.client nunca envia um pacote. .recv() em um HTTPResponse não é .read(). Menciono isso uma vez porque custou tempo e pareceu, por um minuto, que a 12.2 tinha concluído o trabalho.
python3 jellyfin-suggestions-idor-Abraxas-Labs.py
ou