Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

FeedsContactPrivacy© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
jellyfin-suggestions-idor — Jellyfin Server 12.2 - High - Remote - IDOR - GET /Items/Suggestions omit-userId dumps hidden library Path and IDs | Kitploit
Tools/GitHubGitHub/abraxas/jellyfin-suggestions-idor
Vulnerability AnalysisWeb Application ExploitationInformation GatheringWeb SecurityPenetration TestingAPI SecurityLabs & Practice
GitHub
abraxas/jellyfin-suggestions-idor

jellyfin-suggestions-idor

Jellyfin Server 12.2 - High - Remote - IDOR - GET /Items/Suggestions omit-userId dumps hidden library Path and IDs

View Repository
11 day agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

Abraxas Labs - jellyfin-suggestions-idor

abraxaslabs.tech  ·  github.com/abraxas  ·  @abraxas_null  ·  [email protected]  ·  jellyfin-suggestions-idor

jellyfin-suggestions-idor

Jellyfin Server 12.2 - Jellyfin

Class: IDOR. Reach: Remote (any authenticated non-admin).

GET /Items/Suggestions is [Authorize] only. When userId is omitted, SuggestionsController.GetSuggestions sets user = null and never calls RequestHelpers.GetUserId. InternalItemsQuery(null) does not SetUser. LibraryManager.GetItemsResult skips AddUserToQuery. new DtoOptions() includes ItemFields.Path. GetBaseItemDtos(..., user: null) skips IsVisible.

GHSA-9x85 taught GetUserId (omit goes to claims; a foreign GUID is 403). ItemsController.GetItems always uses it. Suggestions did not. Legacy GET /Users/{userId}/Suggestions always has a route GUID, so that sibling does go through GetUserId.

A restricted account dumps titles, IDs, overviews, provider IDs, and filesystem Path for libraries it is not assigned. Those GUIDs play on unauth GET /Videos/{id}/stream.

IDno CVE yet
CWECWE-639 / CWE-862
CVSSHigh: 6.5 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
ProductJellyfin Server
Affectedv12.2 (ca0f16eb7c195f720a1493ed469a27c4657db0c6), image jellyfin/jellyfin:12.2
Authauthenticated (any non-admin, including a child / hidden-library account)
LicenseGNU Affero GPL v3.0
Lab127.0.0.1:18790 only

Leftover #1 of Jellyfin 12.2 leftovers. Same pin, other packs: jellyfin-svg-gzip (GHSA-cf3c leftover gzip data:image/png), jellyfin-playlist-itemids (GHSA-9x85 leftover ItemIds + unauth stream), jellyfin-ffmpeg-colonslash (GHSA-wwwm leftover .strm URL quotes). This pack is the one that dumps the hidden library.

What an attacker can do

Any authenticated non-admin, including a child / hidden-library / MaxParentalRating account. Needs a normal user token. No admin, no API key. They do not change passwords. They do not mint admin. They inventory libraries they were not assigned, including filesystem Path, then play those GUIDs.

Privilege table (exhaustive). Same bar as Signal-Server Dropwizard admin: method + path, what that call grants, recon vs integrity vs availability, lab-proven vs inferred, adjacent surface that stays closed.

AxisMethod + pathGrantsProvenance
ReconGET /Items/Suggestions with no userId (optional limit / mediaType / type)Unscoped recursive item list: Name, Id, Overview, provider IDs, filesystem Path, image metadata of libraries the caller is not assigned, including folders given only to a hidden user. Folder ACL, parental rating, and allowed-tags are not applied.Lab-proven. Restricted labuser (EnableAllFolders=false, EnabledFolders=PublicB only): GET /Items witness=0; Suggestions witness=1
ReconSame with limit omittedInternalItemsQuery.Limit stays unset; the repository returns the full match set (entire item table, random order)Source (SuggestionsController.cs:70-95)
Reconunauth GET /Videos/{itemId}/stream with a GUID from that listBytes of that file as the jellyfin UIDSource (VideosController has no class [Authorize]; StreamingHelpers GetItemById without a user). Stream labbed in sibling leftover jellyfin-playlist-itemids, not this pack
Integritynone on SuggestionsNo password, policy, favorite, or session writeClosed
Availabilitynone on SuggestionsNo DoS beyond ordinary query costClosed

Stays closed on this pin

AxisMethod + pathWhy it is closed
ReconGET /Items?userId= and GET /Items?parentId=<hiddenFolder>Always GetUserId + IsVisible (ItemsController.cs:265, 342-350)
ReconGET /Playlists/{id}/Items?userId=GetUserId (PlaylistsController.cs:524). GHSA-9x85 class, patched
ReconGET /Items/Suggestions?userId=<other>Foreign GUID still hits GetUserId and 403s for a non-admin. Lab-proven (impersonate-403=yes)
Reconlegacy GET /Users/{userId}/SuggestionsRoute GUID is required, so it does call GetUserId
IntegrityPOST /Users/Password?userId=AssertCanUpdateUser
IntegrityPOST /Users/{id}/PolicyRequiresElevation
Integritydevices, sessions, AddAdditionalUserelevation or own-session only
ReconGET /Users / GET /Users/{id}UserDto + policy (folder GUIDs, admin bit), not library contents or hashes. Household enum, not this leftover

I am not claiming plugin-DLL RCE, a reverse shell, or password theft. The dump is the catalog (and Path) of libraries the caller was not assigned. The stream-by-GUID follow-on is longstanding Jellyfin HTML5/DLNA behavior; this leftover is what enumerates the IDs.

How I found it

v12.2 closed the 12.0 GHSA wave. I sat on that tag anyway and read every userId query next to RequestHelpers.GetUserId. Items always uses it. Suggestions branches: empty userId -> user = null, never GetUserId. That is the leftover.

run.sh seeds two movie libraries (PrivateA named with the witness, PublicB), finishes the wizard, creates labuser with EnableAllFolders=false and EnabledFolders=PublicB only, then compares GET /Items against GET /Items/Suggestions.

Wrong turns already recorded: treating GET /Items as the leak (it is scoped); treating Suggestions?userId=<admin> as the leak (403); treating the legacy /Users/{userId}/Suggestions route as the hole (it has a GUID so it calls GetUserId); claiming unauth stream as this pack's bug (sibling leftover); a reverse shell. Theatre.

Client bugs look like product bugs. A helper that shadows http.client never sends a packet. .recv() on an HTTPResponse is not .read(). I mention that once because it cost time and looked, for a minute, like 12.2 had finished the job.

Lab

python3 jellyfin-suggestions-idor-Abraxas-Labs.py

or

cd lab
./run.sh

Target only http://127.0.0.1:18790. Image jellyfin/jellyfin:12.2. Docker required. The launcher chdirs into lab/ and execs run.sh.

IOC user-items http=200 witness=0
IOC user-suggestions http=200 witness=1
IOC impersonate-suggestions http=403
SUCCESS jellyfin-suggestions-idor items-hidden=yes suggestions-has-witness=yes impersonate-403=yes JELLYFIN-SUGGESTIONS-IDOR-WITNESS

Restricted labuser cannot see PrivateA on /Items. The same token sees the witness on /Items/Suggestions with no userId. Passing the admin GUID still 403s.

The fix

Download Tool