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
This commit is contained in:
@@ -60,11 +60,8 @@ public class HlsSegmentController : BaseJellyfinApiController
|
|||||||
public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
|
public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
|
||||||
{
|
{
|
||||||
// TODO: Deprecate with new iOS app
|
// TODO: Deprecate with new iOS app
|
||||||
var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
|
var file = ValidateTranscodePath(string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan())));
|
||||||
var transcodePath = _serverConfigurationManager.GetTranscodePath();
|
if (file is null)
|
||||||
file = Path.GetFullPath(Path.Combine(transcodePath, file));
|
|
||||||
var fileDir = Path.GetDirectoryName(file);
|
|
||||||
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture))
|
|
||||||
{
|
{
|
||||||
return BadRequest("Invalid segment.");
|
return BadRequest("Invalid segment.");
|
||||||
}
|
}
|
||||||
@@ -86,12 +83,9 @@ public class HlsSegmentController : BaseJellyfinApiController
|
|||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
|
||||||
public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
|
public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
|
||||||
{
|
{
|
||||||
var file = string.Concat(playlistId, Path.GetExtension(Request.Path.Value.AsSpan()));
|
var file = ValidateTranscodePath(string.Concat(playlistId, Path.GetExtension(Request.Path.Value.AsSpan())));
|
||||||
var transcodePath = _serverConfigurationManager.GetTranscodePath();
|
if (file is null
|
||||||
file = Path.GetFullPath(Path.Combine(transcodePath, file));
|
|| !Path.GetExtension(file.AsSpan()).Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
|
||||||
var fileDir = Path.GetDirectoryName(file);
|
|
||||||
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture)
|
|
||||||
|| Path.GetExtension(file.AsSpan()).Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
return BadRequest("Invalid segment.");
|
return BadRequest("Invalid segment.");
|
||||||
}
|
}
|
||||||
@@ -140,18 +134,13 @@ public class HlsSegmentController : BaseJellyfinApiController
|
|||||||
[FromRoute, Required] string segmentId,
|
[FromRoute, Required] string segmentId,
|
||||||
[FromRoute, Required] string segmentContainer)
|
[FromRoute, Required] string segmentContainer)
|
||||||
{
|
{
|
||||||
var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
|
var file = ValidateTranscodePath(string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan())));
|
||||||
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
|
if (file is null)
|
||||||
|
|
||||||
file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
|
|
||||||
var fileDir = Path.GetDirectoryName(file);
|
|
||||||
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath, StringComparison.InvariantCulture))
|
|
||||||
{
|
{
|
||||||
return BadRequest("Invalid segment.");
|
return BadRequest("Invalid segment.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var normalizedPlaylistId = playlistId;
|
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
|
||||||
|
|
||||||
var filePaths = _fileSystem.GetFilePaths(transcodeFolderPath);
|
var filePaths = _fileSystem.GetFilePaths(transcodeFolderPath);
|
||||||
// Add . to start of segment container for future use.
|
// Add . to start of segment container for future use.
|
||||||
segmentContainer = segmentContainer.Insert(0, ".");
|
segmentContainer = segmentContainer.Insert(0, ".");
|
||||||
@@ -161,7 +150,7 @@ public class HlsSegmentController : BaseJellyfinApiController
|
|||||||
var pathExtension = Path.GetExtension(path);
|
var pathExtension = Path.GetExtension(path);
|
||||||
if ((string.Equals(pathExtension, segmentContainer, StringComparison.OrdinalIgnoreCase)
|
if ((string.Equals(pathExtension, segmentContainer, StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(pathExtension, ".m3u8", StringComparison.OrdinalIgnoreCase))
|
|| string.Equals(pathExtension, ".m3u8", StringComparison.OrdinalIgnoreCase))
|
||||||
&& path.Contains(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase))
|
&& path.Contains(playlistId, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
playlistPath = path;
|
playlistPath = path;
|
||||||
break;
|
break;
|
||||||
@@ -173,6 +162,19 @@ public class HlsSegmentController : BaseJellyfinApiController
|
|||||||
: GetFileResult(file, playlistPath);
|
: 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)
|
private ActionResult GetFileResult(string path, string playlistPath)
|
||||||
{
|
{
|
||||||
var transcodingJob = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
|
var transcodingJob = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
|
||||||
|
|||||||
@@ -226,10 +226,11 @@ public class PluginsController : BaseJellyfinApiController
|
|||||||
return NotFound();
|
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);
|
imagePath = Path.GetFullPath(imagePath, plugin.Path);
|
||||||
if (!System.IO.File.Exists(imagePath))
|
if (imagePath.StartsWith(plugin.Path, StringComparison.OrdinalIgnoreCase) is false || System.IO.File.Exists(imagePath) is false)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user