Merge remote-tracking branch 'upstream/master' into security-path-traversal-fixes

# Conflicts:
#	Jellyfin.Api/Controllers/HlsSegmentController.cs
#	Jellyfin.Api/Controllers/PluginsController.cs
This commit is contained in:
Shadowghost
2026-07-21 07:14:47 +02:00
79 changed files with 1930 additions and 518 deletions
@@ -6,6 +6,7 @@ using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
using MediaBrowser.Controller.MediaEncoding;
using Microsoft.Extensions.Logging;
@@ -184,8 +185,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
{ "libavdevice", new Version(58, 13) },
{ "libavfilter", new Version(7, 110) },
{ "libswscale", new Version(5, 9) },
{ "libswresample", new Version(3, 9) },
{ "libpostproc", new Version(55, 9) }
{ "libswresample", new Version(3, 9) }
};
private readonly ILogger _logger;
@@ -645,7 +645,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
WindowStyle = ProcessWindowStyle.Hidden,
ErrorDialog = false,
RedirectStandardInput = redirectStandardIn,
StandardOutputEncoding = Encoding.UTF8,
RedirectStandardOutput = true,
StandardErrorEncoding = Encoding.UTF8,
RedirectStandardError = true
}
})
@@ -7,6 +7,7 @@ using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
@@ -528,6 +529,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
UseShellExecute = false,
// Must consume both or ffmpeg may hang due to deadlocks.
StandardOutputEncoding = Encoding.UTF8,
RedirectStandardOutput = true,
FileName = _ffprobePath,
@@ -926,6 +928,25 @@ namespace MediaBrowser.MediaEncoding.Encoder
throw new InvalidOperationException("EncodingHelper returned empty or invalid filter parameters.");
}
// Normalize invalid PTS from containers for non keyframe only mode
if (!enableKeyFrameOnlyExtraction)
{
var fpsFilterIndex = filterParam.IndexOf("fps=", StringComparison.Ordinal);
if (fpsFilterIndex >= 0)
{
var inputFrameRate = (imageStream.ReferenceFrameRate.HasValue && imageStream.ReferenceFrameRate > 0)
? imageStream.ReferenceFrameRate.Value : 30;
var setPtsFilter = string.Create(CultureInfo.InvariantCulture, $"setpts=N/{inputFrameRate:F3}/TB,");
filterParam = filterParam.Insert(fpsFilterIndex, setPtsFilter);
}
else
{
throw new InvalidOperationException("EncodingHelper returned invalid filter parameters.");
}
}
try
{
return await ExtractVideoImagesOnIntervalInternal(
@@ -1,7 +1,7 @@
#pragma warning disable CS1591
using System.IO;
using MediaBrowser.Model.MediaInfo;
using Nikse.SubtitleEdit.Core.Common;
namespace MediaBrowser.MediaEncoding.Subtitles
{
@@ -12,8 +12,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="fileExtension">The file extension.</param>
/// <returns>SubtitleTrackInfo.</returns>
SubtitleTrackInfo Parse(Stream stream, string fileExtension);
/// <returns>The parsed subtitle.</returns>
Subtitle Parse(Stream stream, string fileExtension);
/// <summary>
/// Determines whether the file extension is supported by the parser.
@@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Jellyfin.Extensions;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
using Nikse.SubtitleEdit.Core.Common;
using SubtitleFormat = Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat;
@@ -30,7 +28,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
}
/// <inheritdoc />
public SubtitleTrackInfo Parse(Stream stream, string fileExtension)
public Subtitle Parse(Stream stream, string fileExtension)
{
var subtitle = new Subtitle();
var lines = stream.ReadAllLines().ToList();
@@ -76,21 +74,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
throw new ArgumentException("Unsupported format: " + fileExtension);
}
var trackInfo = new SubtitleTrackInfo();
int len = subtitle.Paragraphs.Count;
var trackEvents = new SubtitleTrackEvent[len];
for (int i = 0; i < len; i++)
{
var p = subtitle.Paragraphs[i];
trackEvents[i] = new SubtitleTrackEvent(p.Number.ToString(CultureInfo.InvariantCulture), p.Text)
{
StartPositionTicks = p.StartTime.TimeSpan.Ticks,
EndPositionTicks = p.EndTime.TimeSpan.Ticks
};
}
trackInfo.TrackEvents = trackEvents;
return trackInfo;
return subtitle;
}
/// <inheritdoc />
@@ -73,7 +73,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
_serverConfigurationManager = serverConfigurationManager;
}
private MemoryStream ConvertSubtitles(
internal MemoryStream ConvertSubtitles(
Stream stream,
SubtitleInfo inputInfo,
string outputFormat,
@@ -81,7 +81,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
long endTimeTicks,
bool preserveOriginalTimestamps)
{
var subtitle = Subtitle.Parse(stream, Path.GetExtension(inputInfo.Path));
var subtitle = _subtitleParser.Parse(stream, inputInfo.Format);
FilterEvents(subtitle, startTimeTicks, endTimeTicks, preserveOriginalTimestamps);
@@ -445,98 +445,15 @@ namespace MediaBrowser.MediaEncoding.Subtitles
encodingParam = " -sub_charenc " + encodingParam;
}
int exitCode;
var args = string.Format(CultureInfo.InvariantCulture, "-y {0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath);
using (var process = new Process
{
StartInfo = new ProcessStartInfo
{
CreateNoWindow = true,
UseShellExecute = false,
FileName = _mediaEncoder.EncoderPath,
Arguments = string.Format(CultureInfo.InvariantCulture, "-y {0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath),
WindowStyle = ProcessWindowStyle.Hidden,
ErrorDialog = false
},
EnableRaisingEvents = true
})
{
_logger.LogInformation("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
try
{
process.Start();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error starting ffmpeg");
throw;
}
try
{
var timeoutMinutes = _serverConfigurationManager.GetEncodingOptions().SubtitleExtractionTimeoutMinutes;
await process.WaitForExitAsync(TimeSpan.FromMinutes(timeoutMinutes)).ConfigureAwait(false);
exitCode = process.ExitCode;
}
catch (OperationCanceledException)
{
process.Kill(true);
exitCode = -1;
}
}
var failed = false;
if (exitCode == -1)
{
failed = true;
if (File.Exists(outputPath))
{
try
{
_logger.LogInformation("Deleting converted subtitle due to failure: {Path}", outputPath);
_fileSystem.DeleteFile(outputPath);
}
catch (IOException ex)
{
_logger.LogError(ex, "Error deleting converted subtitle {Path}", outputPath);
}
}
}
else if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
{
failed = true;
try
{
_logger.LogWarning("Deleting converted subtitle due to failure: {Path}", outputPath);
_fileSystem.DeleteFile(outputPath);
}
catch (FileNotFoundException)
{
}
catch (IOException ex)
{
_logger.LogError(ex, "Error deleting converted subtitle {Path}", outputPath);
}
}
if (failed)
{
_logger.LogError("ffmpeg subtitle conversion failed for {Path}", inputPath);
throw new FfmpegException(
string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle conversion failed for {0}", inputPath));
}
await SetAssFont(outputPath, cancellationToken).ConfigureAwait(false);
await ExtractSubtitlesForFile(
inputPath,
args,
[outputPath],
cancellationToken).ConfigureAwait(false);
WriteCacheMeta(outputPath, inputPath);
_logger.LogInformation("ffmpeg subtitle conversion succeeded for {Path}", inputPath);
}
private string GetExtractableSubtitleFormat(MediaStream subtitleStream)
@@ -727,7 +644,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var outputPaths = new List<string>();
var args = string.Format(
CultureInfo.InvariantCulture,
"-i {0}",
"-y -i {0}",
inputPath);
foreach (var subtitleStream in subtitleStreams)
@@ -781,50 +698,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles
private async Task ExtractSubtitlesForFile(
string inputPath,
string args,
List<string> outputPaths,
IReadOnlyList<string> outputPaths,
CancellationToken cancellationToken)
{
int exitCode;
using (var process = new Process
{
StartInfo = new ProcessStartInfo
{
CreateNoWindow = true,
UseShellExecute = false,
FileName = _mediaEncoder.EncoderPath,
Arguments = args,
WindowStyle = ProcessWindowStyle.Hidden,
ErrorDialog = false
},
EnableRaisingEvents = true
})
{
_logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
try
{
process.Start();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error starting ffmpeg");
throw;
}
try
{
var timeoutMinutes = _serverConfigurationManager.GetEncodingOptions().SubtitleExtractionTimeoutMinutes;
await process.WaitForExitAsync(TimeSpan.FromMinutes(timeoutMinutes)).ConfigureAwait(false);
exitCode = process.ExitCode;
}
catch (OperationCanceledException)
{
process.Kill(true);
exitCode = -1;
}
}
var (exitCode, ffmpegError) = await RunSubtitleExtractionProcess(args, cancellationToken).ConfigureAwait(false);
var failed = false;
@@ -884,6 +761,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
if (failed)
{
cancellationToken.ThrowIfCancellationRequested();
if (!string.IsNullOrWhiteSpace(ffmpegError))
{
_logger.LogError("ffmpeg subtitle extraction failed for {InputPath}: {FfmpegOutput}", inputPath, ffmpegError);
}
throw new FfmpegException(
string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle extraction failed for {0}", inputPath));
}
@@ -941,16 +825,38 @@ namespace MediaBrowser.MediaEncoding.Subtitles
ArgumentException.ThrowIfNullOrEmpty(outputPath);
Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath)));
var processArgs = string.Format(
CultureInfo.InvariantCulture,
"-i {0} -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"",
"-y -i {0} -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"",
inputPath,
subtitleStreamIndex,
outputCodec,
outputPath);
await ExtractSubtitlesForFile(
inputPath,
processArgs,
[outputPath],
cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Runs ffmpeg to extract or convert subtitles, capturing its exit code and stderr output.
/// </summary>
/// <remarks>
/// stdin is redirected and closed, and <c>-nostdin</c> is prepended to the arguments, so ffmpeg can never
/// block reading an inherited stdin handle (which happens when Jellyfin runs as a service, e.g. under NSSM,
/// and stalls subtitle extraction until the timeout). stderr is redirected and drained so a full pipe buffer
/// cannot deadlock ffmpeg and so its output can be surfaced on failure; stdout is left un-redirected as it is
/// unused for subtitle extraction.
/// </remarks>
/// <param name="arguments">The ffmpeg command line arguments.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The ffmpeg exit code (-1 on timeout) and its captured stderr output.</returns>
private async Task<(int ExitCode, string StandardError)> RunSubtitleExtractionProcess(string arguments, CancellationToken cancellationToken)
{
int exitCode;
var standardError = string.Empty;
using (var process = new Process
{
@@ -958,8 +864,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardError = true,
FileName = _mediaEncoder.EncoderPath,
Arguments = processArgs,
Arguments = "-nostdin " + arguments,
WindowStyle = ProcessWindowStyle.Hidden,
ErrorDialog = false
},
@@ -975,14 +883,21 @@ namespace MediaBrowser.MediaEncoding.Subtitles
catch (Exception ex)
{
_logger.LogError(ex, "Error starting ffmpeg");
throw;
}
// Close stdin so ffmpeg observes EOF instead of blocking on an inherited handle.
process.StandardInput.Close();
// Begin draining stderr before waiting for exit; a full stderr pipe buffer would otherwise deadlock ffmpeg.
var standardErrorTask = process.StandardError.ReadToEndAsync(CancellationToken.None);
var timeoutMinutes = _serverConfigurationManager.GetEncodingOptions().SubtitleExtractionTimeoutMinutes;
using var waitSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
waitSource.CancelAfter(TimeSpan.FromMinutes(timeoutMinutes));
try
{
var timeoutMinutes = _serverConfigurationManager.GetEncodingOptions().SubtitleExtractionTimeoutMinutes;
await process.WaitForExitAsync(TimeSpan.FromMinutes(timeoutMinutes)).ConfigureAwait(false);
await process.WaitForExitAsync(waitSource.Token).ConfigureAwait(false);
exitCode = process.ExitCode;
}
catch (OperationCanceledException)
@@ -990,59 +905,18 @@ namespace MediaBrowser.MediaEncoding.Subtitles
process.Kill(true);
exitCode = -1;
}
}
var failed = false;
if (exitCode == -1)
{
failed = true;
try
{
_logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath);
_fileSystem.DeleteFile(outputPath);
standardError = await standardErrorTask.ConfigureAwait(false);
}
catch (FileNotFoundException)
catch (OperationCanceledException)
{
}
catch (IOException ex)
{
_logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath);
}
}
else if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
{
failed = true;
try
{
_logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath);
_fileSystem.DeleteFile(outputPath);
}
catch (FileNotFoundException)
{
}
catch (IOException ex)
{
_logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath);
// Reading ffmpeg output was cancelled; nothing more to capture.
}
}
if (failed)
{
_logger.LogError("ffmpeg subtitle extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath);
throw new FfmpegException(
string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle extraction failed for {0} to {1}", inputPath, outputPath));
}
_logger.LogInformation("ffmpeg subtitle extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath);
if (string.Equals(outputCodec, "ass", StringComparison.OrdinalIgnoreCase))
{
await SetAssFont(outputPath, cancellationToken).ConfigureAwait(false);
}
return (exitCode, standardError);
}
/// <summary>
@@ -424,6 +424,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
// Must consume both stdout and stderr or deadlocks may occur
// RedirectStandardOutput = true,
StandardErrorEncoding = Encoding.UTF8,
RedirectStandardError = true,
RedirectStandardInput = true,
FileName = _mediaEncoder.EncoderPath,