Research/jellyfin-12.2-leftovers
Series0-dayNo CVEHighPublic

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:

  1. jellyfin-suggestions-idor - GET /Items/Suggestions omit-userId
  2. jellyfin-svg-gzip - gzip data:image/png leftover of GHSA-cf3c
  3. jellyfin-playlist-itemids - public playlist ItemIds leftover of GHSA-9x85
  4. jellyfin-ffmpeg-colonslash - .strm URL 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:

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 /Items witness=0. GET /Items/Suggestions witness=1. Impersonate 403.
  • SVG: 127.0.0.1:18780 plus oracle :18781. Negative direct http 0 hits. Gzip-png wrapper 1 hit.
  • Playlist: 127.0.0.1:18800. AllowedTags so /Items drops the private child. ItemIds still lists it. Unauth static=true stream 200, witness in body.
  • FFmpeg: 127.0.0.1:18810. Control .mkv one -i file:"…". Plant .strm quote-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