Jellyfin 12.2 leftovers
Jellyfin Server v12.2 closed the 12.0 GHSA wave. GET /Items/Suggestions with no userId still dumps hidden libraries including Path. Gzip data:image/png still walks past SvgSecurityValidator. Public playlist ItemIds still lists private GUIDs. A library .strm URL with a quote still splits ffmpeg -i.
- Name
- Jellyfin 12.2 leftovers
- Series
- Jellyfin 12.2 leftovers
- Phase
- 0 · The pin
- Type
- 0-day analysis
- CVE
- n/a
- CVE Risk
- high
- Disclosure Status
- public
- Vendor
- Jellyfin
- Affected
- Jellyfin Server v12.2 (ca0f16eb7c195f720a1493ed469a27c4657db0c6). Image jellyfin/jellyfin:12.2.
- Published
- 17 Oct 2026
- Updated
- 17 Oct 2026
- Tags
- 0-day, jellyfin, idor, ssrf, ffmpeg, leftover, remote
The 12.0 wave named the fences. Four paths still walk around them.
A named GHSA is a fence. The leftover is whatever still walks around it.
I am abraxas. This is the series hub for four leftovers I labbed on Jellyfin Server v12.2 (ca0f16eb7c195f720a1493ed469a27c4657db0c6), posted as @abraxas_null. The image is jellyfin/jellyfin:12.2. DefaultPolicy is [Authorize]. Endpoints without it are anonymous. Loopback witness labs. No shells.
v12.2 closed the September 2026 12.0 advisory wave. GHSA-9x85 taught RequestHelpers.GetUserId. GHSA-cf3c taught Svg.Skia not to fetch outer http and file hrefs. GHSA-wwwm escaped the file: ffmpeg -i branch. Four handlers next to those patches did not get the lesson.
The leftover I would patch first is Suggestions. A child account, hidden libraries, [Authorize] only. Omit userId. The server dumps Path. Those GUIDs play on unauth stream.
The four posts, in that order:
- jellyfin-suggestions-idor - GET /Items/Suggestions omit-userId
- jellyfin-svg-gzip - gzip
data:image/pngleftover of GHSA-cf3c - jellyfin-playlist-itemids - public playlist ItemIds leftover of GHSA-9x85
- jellyfin-ffmpeg-colonslash -
.strmURL quotes leftover of GHSA-wwwm
Vendor intake is security@jellyfin.org, subject [Jellyfin Security]. I mailed the four as one thread. Packs are AGPLv3 under github.com/abraxas. Labs talk to loopback and print a witness.
Why this product
Jellyfin is the media server people actually self-host. Libraries are the product. A child profile that cannot see PrivateA is a promise. Stream-by-GUID has been around forever. The interesting leftover is the enumerator that hands you the GUID of a library you were never assigned.
I wanted unpublished Critical or High on the current GA, remote preferred. jellyfin-web XSS, Wizarr, Seerr, and jellyfin-ios are other trees. I did not file them.
The security model is honest once you look at it. [Authorize] is DefaultPolicy. First-run wizard is a separate handler while IsStartupWizardCompleted is false. POST /Startup/User forbids if the first user already has a password. EnsureStartupWizardIntegrity restores the flag when any admin has a password. Empty KnownProxies means ForwardedHeaders.None. That is the GHSA-qcmf close on default. I needed the handler that skipped GetUserId, the SVG pre-scan that treated gzip as opaque raster, the playlist DTO that still shipped unfiltered ItemIds, and the ffmpeg -i branch PR 17685 did not quote-escape.
How the pin was chosen
Latest GA. Tag v12.2. Peel is ca0f16eb7c195f720a1493ed469a27c4657db0c6. Image jellyfin/jellyfin:12.2 reports 12.2.0. I did not chase master. A leftover on a GA tag is a leftover someone can reproduce.
Labs use that image on 127.0.0.1. Replay is ./run.sh in each pack. Wizard, two libraries, a restricted user, a witness string in a filename or an oracle log.
The skip list is half the hunt
Before you look for leftovers, you write down what is already public so you do not "discover" a CVE with a press release. Twenty-three published GitHub advisories on this tree. I did not re-file:
- GHSA-cf3c-8m59-2vhx SVG Skia external refs. Outer
http/filehrefs are refused. The gzipdata:image/pngwrapper is the leftover. - GHSA-9x85-gx46-6522 Playlist Items userId IDOR.
/Itemsis filtered.ItemIdsonGET /Playlists/{id}is not. - GHSA-wwwm-px48-fpvq FFmpeg subtitle path argument injection, then PR 17685 on the
file:-ibranch. Non-File protocol is the leftover. - GHSA-6828-c7cx-hvqm VirtualFolders
Path.Combinerooted second arg. - GHSA-qcmf-gmhm-rfv9 X-Forwarded-For. Default
KnownProxiesempty. - GHSA-4vx8-xhc9-qg6x session remote-control.
- GHSA-8fw7-f233-ffr8 LiveTV M3U.
- GHSA-j2hf-x4q5-47j3 subtitle upload
.strm. - GHSA-jh22-fw8w-2v9x StreamOptions
levelTryParse. - GHSA-hw73-62vp-9gxf / GHSA-f47c-m7gr-q92j HLS / MKV attachment.
- GHSA-jg92-mrxq-vv75 / GHSA-9p5f-5x8v-x65m ClientLog.
- GHSA-rrr6-mvwg-9pg9 branding splash.
- GHSA-rr9h-w522-cvmr encoder path.
- GHSA-wg4c-c9g9-rxhx unauth library download.
- GHSA-rgjw-4fwc-9v96 unauth remote image GET.
POST /Items/{id}/RemoteImages/Download?imageUrl=is still a raw artwork fetch withRequiresElevation. Intended. Dropped.
Hunts that returned nothing in this bar: first-run wizard, path-combine (VirtualFolders / ClientLog / HLS / plugin image / attachments / subtitles / backup zip), plugins, QuickConnect, backup, DLNA (plugin-only), SyncPlay, transcode/trickplay, WebSocket, system/config.
Four leftovers, one pin
| # | Pack | Reach | Class | What an attacker can do |
|---|---|---|---|---|
| 1 | jellyfin-suggestions-idor | Remote | IDOR | Omit userId on Suggestions and dump another user's private library (names, IDs, Path). Those GUIDs play on unauth stream. |
| 2 | jellyfin-svg-gzip | Remote | SSRF + file read + memex | Upload a profile SVG whose data:image/png payload is gzip of an inner SVG. The server still fetches http(s) / file:// while rasterizing. |
| 3 | jellyfin-playlist-itemids | Remote | IDOR | Read private Item GUIDs from GET /Playlists/{id} and stream them unauth. |
| 4 | jellyfin-ffmpeg-colonslash | Remote after a library .strm plant |
FFmpeg arg injection | Plant a .strm whose URL contains ", then unauth /Videos/{id}/stream splits ffmpeg argv. Not RCE. |
Suggestions is first because it is the enumerator. A hidden library is supposed to be invisible. This handler never asked who you were.
Playlist ItemIds is the sibling of the same GHSA. /Items learned IsVisible. The DTO that ships ItemIds did not. Unauth stream was already there. Together they turn a public playlist into a GUID oracle for a private movie.
The SVG leftover is a pre-scan that trusted a media type. image/png looked like a raster. Gzip magic plus Svg.Skia 3.7.0 made it a document again.
The ffmpeg leftover is a quote. PR 17685 escaped file:. Http .strm still wraps the raw URL. A POSIX filename with :// does not reach this sink. Slash is a directory.
How I labbed them
Four independent compose projects. Unique loopback ports. jellyfin/jellyfin:12.2. Witness strings in filenames, oracle logs, or TranscodeManager argv. Stacks torn down.
- Suggestions: 127.0.0.1:18790. Restricted
labuser.GET /Itemswitness=0.GET /Items/Suggestionswitness=1. Impersonate 403. - SVG: 127.0.0.1:18780 plus oracle :18781. Negative direct
http0 hits. Gzip-png wrapper 1 hit. - Playlist: 127.0.0.1:18800.
AllowedTagsso/Itemsdrops the private child.ItemIdsstill lists it. Unauthstatic=truestream 200, witness in body. - FFmpeg: 127.0.0.1:18810. Control
.mkvone-i file:"…". Plant.strmquote-break in the log.
Replay: clone the pack, python3 *-Abraxas-Labs.py or cd lab && ./run.sh.
Contact
abraxaslabs.tech · github.com/abraxas · @abraxas_null · abraxas.null@proton.me