From f8ffccae7fa65a27324667ccd156ca71fbfc89cd Mon Sep 17 00:00:00 2001 From: Nils Lehnen Date: Sat, 4 Jul 2026 23:55:31 +0200 Subject: Use InvariantCulture when parsing machine-generated dates DateTime.TryParse without an IFormatProvider falls back to the current thread culture, so the same string can parse differently (or fail) depending on the server's locale. None of these call sites deal with user-entered text - they parse dates that come from filenames, an HTTP header, ffprobe metadata and values the app itself wrote to the auth database - so InvariantCulture is the correct provider everywhere here. Fixes the S6580 / CA1305 warnings on these call sites. Co-Authored-By: Claude Opus 4.8 --- Jellyfin.Api/Controllers/ImageController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'Jellyfin.Api/Controllers/ImageController.cs') diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index ae792142b4..52d8b4dad1 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -2049,7 +2049,7 @@ public class ImageController : BaseJellyfinApiController } // Check If-Modified-Since header for time-based validation - if (DateTime.TryParse(Request.Headers[HeaderNames.IfModifiedSince], out var ifModifiedSinceHeader)) + if (DateTime.TryParse(Request.Headers[HeaderNames.IfModifiedSince], CultureInfo.InvariantCulture, out var ifModifiedSinceHeader)) { // Return 304 if the image has not been modified since the client's cached version if (dateImageModified <= ifModifiedSinceHeader) -- cgit v1.2.3 From 21801e8ba138af71c4c58489ea33534adf7426c5 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Fri, 17 Jul 2026 16:51:19 +0200 Subject: Harden remaining path-construction sinks against traversal --- Jellyfin.Api/Controllers/HlsSegmentController.cs | 10 ++++------ Jellyfin.Api/Controllers/ImageController.cs | 8 +++++++- Jellyfin.Api/Controllers/PluginsController.cs | 5 +++-- Jellyfin.Server.Implementations/Users/UserManager.cs | 2 +- .../Users/UserManagerTests.cs | 2 ++ 5 files changed, 17 insertions(+), 10 deletions(-) (limited to 'Jellyfin.Api/Controllers/ImageController.cs') diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index b5365cd632..1b432aaf44 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -5,6 +5,7 @@ using System.IO; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Helpers; +using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; @@ -63,8 +64,7 @@ public class HlsSegmentController : BaseJellyfinApiController 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)) + if (!PathHelper.IsContainedIn(transcodePath, file)) { return BadRequest("Invalid segment."); } @@ -89,8 +89,7 @@ public class HlsSegmentController : BaseJellyfinApiController 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) + if (!PathHelper.IsContainedIn(transcodePath, file) || Path.GetExtension(file.AsSpan()).Equals(".m3u8", StringComparison.OrdinalIgnoreCase)) { return BadRequest("Invalid segment."); @@ -144,8 +143,7 @@ public class HlsSegmentController : BaseJellyfinApiController 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)) + if (!PathHelper.IsContainedIn(transcodeFolderPath, file)) { return BadRequest("Invalid segment."); } diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 52d8b4dad1..d492a2f5ba 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -125,7 +125,13 @@ public class ImageController : BaseJellyfinApiController { // Handle image/png; charset=utf-8 var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); - var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username); + var userConfigurationDirectoryPath = _serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath; + var userDataPath = Path.Combine(userConfigurationDirectoryPath, user.Username); + if (!PathHelper.IsContainedIn(userConfigurationDirectoryPath, userDataPath)) + { + return BadRequest("Invalid user."); + } + if (user.ProfileImage is not null) { await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index 0105ecf7a7..3bb8bb7077 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Api.Attributes; +using Jellyfin.Extensions; using Jellyfin.Extensions.Json; using MediaBrowser.Common.Api; using MediaBrowser.Common.Plugins; @@ -228,8 +229,8 @@ public class PluginsController : BaseJellyfinApiController if (!string.IsNullOrEmpty(plugin.Manifest.ImagePath)) { - var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath); - if (!System.IO.File.Exists(imagePath)) + var imagePath = Path.GetFullPath(Path.Combine(plugin.Path, plugin.Manifest.ImagePath)); + if (!PathHelper.IsContainedIn(plugin.Path, imagePath) || !System.IO.File.Exists(imagePath)) { return NotFound(); } diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 583f29f94f..136d2d6029 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -893,7 +893,7 @@ namespace Jellyfin.Server.Implementations.Users internal static void ThrowIfInvalidUsername(string name) { - if (!string.IsNullOrWhiteSpace(name) && ValidUsernameRegex().IsMatch(name)) + if (!string.IsNullOrWhiteSpace(name) && ValidUsernameRegex().IsMatch(name) && name != "." && name != "..") { return; } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerTests.cs index 4cea53bd3d..2bf1d1d05b 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerTests.cs @@ -27,6 +27,8 @@ namespace Jellyfin.Server.Implementations.Tests.Users [InlineData(" thishasaspaceatthestart")] [InlineData(" thishasaspaceatbothends ")] [InlineData(" this has a space at both ends and inbetween ")] + [InlineData(".")] + [InlineData("..")] public void ThrowIfInvalidUsername_WhenInvalidUsername_ThrowsArgumentException(string username) { Assert.Throws(() => UserManager.ThrowIfInvalidUsername(username)); -- cgit v1.2.3