diff options
| author | Marc Brooks <IDisposable@gmail.com> | 2026-04-01 16:55:47 -0500 |
|---|---|---|
| committer | Marc Brooks <IDisposable@gmail.com> | 2026-06-26 12:05:54 -0500 |
| commit | 617ebf367ff24ecbe8e0de5aebc90fe28689bcb0 (patch) | |
| tree | 4710b01b3da7cd18ab3396a88243097792f5dbe9 | |
| parent | b9db4566a74ec94bcd24e7333b9d0cc6156e2e25 (diff) | |
Fix path transversal exposure in Plugins
The request path is not validated to a valid path and could allow escaping the transcode path and downloading of any arbitrary file in GetHlsPlaylistLegacy .
GetHlsAudioSegmentLegacy and GetHlsVideoSegmentLegacy have the same issue, and are NOT behind an Authorize so they are publicly exploitable.
Added a ValidateTranscodePath that verifies that requested file paths start with the transcode path setting. Also ensure that all filename comparisons are OrdinalIgnoreCase because we might be running on a filesystem where filename-casing doesn't have to match. Switched from InvariantCulture because the underlying OS filename comparisons are always byte-wise (with case insensitivity here).
Fixed a similar issue in GetPluginImage
| -rw-r--r-- | Jellyfin.Api/Controllers/HlsSegmentController.cs | 42 | ||||
| -rw-r--r-- | Jellyfin.Api/Controllers/PluginsController.cs | 7 |
2 files changed, 26 insertions, 23 deletions
diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index b5365cd632..4cb7627559 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -60,11 +60,8 @@ public class HlsSegmentController : BaseJellyfinApiController public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId) { // TODO: Deprecate with new iOS app - var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan())); - var transcodePath = _serverConfigurationManager.GetTranscodePath(); - file = Path.GetFullPath(Path.Combine(transcodePath, file)); - var fileDir = Path.GetDirectoryName(file); - if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture)) + var file = ValidateTranscodePath(string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()))); + if (file is null) { return BadRequest("Invalid segment."); } @@ -86,12 +83,9 @@ public class HlsSegmentController : BaseJellyfinApiController [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId) { - var file = string.Concat(playlistId, Path.GetExtension(Request.Path.Value.AsSpan())); - var transcodePath = _serverConfigurationManager.GetTranscodePath(); - file = Path.GetFullPath(Path.Combine(transcodePath, file)); - var fileDir = Path.GetDirectoryName(file); - if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) - || Path.GetExtension(file.AsSpan()).Equals(".m3u8", StringComparison.OrdinalIgnoreCase)) + var file = ValidateTranscodePath(string.Concat(playlistId, Path.GetExtension(Request.Path.Value.AsSpan()))); + if (file is null + || !Path.GetExtension(file.AsSpan()).Equals(".m3u8", StringComparison.OrdinalIgnoreCase)) { return BadRequest("Invalid segment."); } @@ -140,18 +134,13 @@ public class HlsSegmentController : BaseJellyfinApiController [FromRoute, Required] string segmentId, [FromRoute, Required] string segmentContainer) { - var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan())); - var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath(); - - file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file)); - var fileDir = Path.GetDirectoryName(file); - if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath, StringComparison.InvariantCulture)) + var file = ValidateTranscodePath(string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()))); + if (file is null) { return BadRequest("Invalid segment."); } - var normalizedPlaylistId = playlistId; - + var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath(); var filePaths = _fileSystem.GetFilePaths(transcodeFolderPath); // Add . to start of segment container for future use. segmentContainer = segmentContainer.Insert(0, "."); @@ -161,7 +150,7 @@ public class HlsSegmentController : BaseJellyfinApiController var pathExtension = Path.GetExtension(path); if ((string.Equals(pathExtension, segmentContainer, StringComparison.OrdinalIgnoreCase) || string.Equals(pathExtension, ".m3u8", StringComparison.OrdinalIgnoreCase)) - && path.Contains(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase)) + && path.Contains(playlistId, StringComparison.OrdinalIgnoreCase)) { playlistPath = path; break; @@ -173,6 +162,19 @@ public class HlsSegmentController : BaseJellyfinApiController : GetFileResult(file, playlistPath); } + private string? ValidateTranscodePath(string filename) + { + var transcodePath = _serverConfigurationManager.GetTranscodePath(); + var file = Path.GetFullPath(filename, transcodePath); + var fileDir = Path.GetDirectoryName(file); + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + return file; + } + private ActionResult GetFileResult(string path, string playlistPath) { var transcodingJob = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls); diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index 0105ecf7a7..5f6136ed11 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -226,10 +226,11 @@ public class PluginsController : BaseJellyfinApiController return NotFound(); } - if (!string.IsNullOrEmpty(plugin.Manifest.ImagePath)) + string? imagePath = plugin.Manifest.ImagePath; + if (!string.IsNullOrWhiteSpace(imagePath)) { - var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath); - if (!System.IO.File.Exists(imagePath)) + imagePath = Path.GetFullPath(imagePath, plugin.Path); + if (imagePath.StartsWith(plugin.Path, StringComparison.OrdinalIgnoreCase) is false || System.IO.File.Exists(imagePath) is false) { return NotFound(); } |
