Secure library paths

This commit is contained in:
Shadowghost
2026-08-25 09:28:45 +02:00
parent d6bcad3b59
commit 0c560b22ce
5 changed files with 165 additions and 10 deletions
@@ -0,0 +1,60 @@
using System;
using System.IO;
using MediaBrowser.Controller.IO;
using Xunit;
namespace Jellyfin.Controller.Tests.IO;
public class FileSystemHelperTests
{
private static readonly string _parentPath = Path.Combine(Path.GetTempPath(), "jellyfin-test", "root", "default");
[Theory]
[InlineData("Movies")]
[InlineData("My Movies")]
[InlineData("..2")]
[InlineData("...")]
[InlineData("a.b")]
public void GetChildPath_ValidName_ReturnsPathInsideParent(string name)
{
var path = FileSystemHelper.GetChildPath(_parentPath, name);
Assert.Equal(Path.Combine(_parentPath, name), path);
}
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData(".")]
[InlineData("..")]
[InlineData("../..")]
[InlineData("../../etc")]
[InlineData("Movies/../..")]
[InlineData("/var/lib/jellyfin/data")]
[InlineData("sub/folder")]
[InlineData("with\0null")]
public void GetChildPath_EscapingName_ReturnsNull(string name)
{
Assert.Null(FileSystemHelper.GetChildPath(_parentPath, name));
}
[Theory]
[InlineData("..\\..")]
[InlineData("sub\\folder")]
[InlineData("C:\\Windows")]
public void GetChildPath_WindowsSeparator_DoesNotEscapeParent(string name)
{
var path = FileSystemHelper.GetChildPath(_parentPath, name);
// On Windows these are rejected outright, on other platforms a backslash is a legal file name character.
Assert.True(path is null || string.Equals(Path.GetDirectoryName(path), _parentPath, StringComparison.Ordinal));
}
[Fact]
public void GetChildPath_ParentWithTrailingSeparator_ReturnsPathInsideParent()
{
var path = FileSystemHelper.GetChildPath(_parentPath + Path.DirectorySeparatorChar, "Movies");
Assert.Equal(Path.Combine(_parentPath, "Movies"), path);
}
}
@@ -114,6 +114,58 @@ public sealed class LibraryStructureControllerTests : IClassFixture<JellyfinAppl
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Theory]
[Priority(1)]
[InlineData("..")]
[InlineData("../..")]
[InlineData(".")]
[InlineData("test/../..")]
[InlineData("/var/lib/jellyfin/data")]
public async Task DeleteLibrary_PathTraversal_NotFound(string name)
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
using var response = await client.DeleteAsync($"Library/VirtualFolders?name={Uri.EscapeDataString(name)}", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Theory]
[Priority(1)]
[InlineData("..")]
[InlineData("../..")]
[InlineData(".")]
[InlineData("test/../..")]
[InlineData("/var/lib/jellyfin/data")]
public async Task RenameLibrary_PathTraversalNewName_BadRequest(string newName)
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
using var response = await client.PostAsync(
$"Library/VirtualFolders/Name?name=test&newName={Uri.EscapeDataString(newName)}",
null,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Theory]
[Priority(1)]
[InlineData("..")]
[InlineData("../..")]
[InlineData("/var/lib/jellyfin/data")]
public async Task RenameLibrary_PathTraversalName_NotFound(string name)
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
using var response = await client.PostAsync(
$"Library/VirtualFolders/Name?name={Uri.EscapeDataString(name)}&newName=renamed",
null,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
[Priority(1)]
public async Task DeleteLibrary_Valid_Success()