aboutsummaryrefslogtreecommitdiff
path: root/tests/Jellyfin.Api.Tests/Controllers/PluginsControllerTests.cs
blob: f040a328bbd12013fe5a6fe1ead4403270acc780 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
using System;
using System.IO;
using Jellyfin.Api.Controllers;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Moq;
using Xunit;

namespace Jellyfin.Api.Tests.Controllers;

// Covers the path-traversal validation in GetPluginImage: a plugin's manifest ImagePath
// must resolve to a file inside the plugin's own directory.
public sealed class PluginsControllerTests
{
    private readonly Mock<IPluginManager> _pluginManager = new();
    private readonly string _pluginPath;

    public PluginsControllerTests()
    {
        _pluginPath = Path.Combine(Path.GetTempPath(), "jellyfin-plugin-image-tests");
        Directory.CreateDirectory(_pluginPath);
    }

    private PluginsController CreateController() =>
        new PluginsController(Mock.Of<IInstallationManager>(), _pluginManager.Object)
        {
            ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }
        };

    private void SetupPlugin(Guid id, Version version, string? imagePath)
    {
        var manifest = new PluginManifest { Id = id, Name = "Test", Version = version.ToString(), ImagePath = imagePath };
        _pluginManager.Setup(p => p.GetPlugin(id, version))
            .Returns(new LocalPlugin(_pluginPath, true, manifest));
    }

    [Fact]
    public void GetPluginImage_UnknownPlugin_ReturnsNotFound()
    {
        var result = CreateController().GetPluginImage(Guid.NewGuid(), new Version(1, 0));

        Assert.IsType<NotFoundResult>(result);
    }

    [Fact]
    public void GetPluginImage_ImageInsidePluginPath_ReturnsFile()
    {
        var id = Guid.NewGuid();
        var version = new Version(1, 0);
        File.WriteAllBytes(Path.Combine(_pluginPath, "logo.png"), Array.Empty<byte>());
        SetupPlugin(id, version, "logo.png");

        var result = CreateController().GetPluginImage(id, version);

        Assert.IsType<PhysicalFileResult>(result);
    }

    [Fact]
    public void GetPluginImage_ImageInsidePluginPathButMissing_ReturnsNotFound()
    {
        var id = Guid.NewGuid();
        var version = new Version(1, 0);
        SetupPlugin(id, version, "does-not-exist.png");

        var result = CreateController().GetPluginImage(id, version);

        Assert.IsType<NotFoundResult>(result);
    }

    [Theory]
    [InlineData("../../../../etc/passwd")]
    [InlineData("subdir/../../../../etc/passwd")]
    public void GetPluginImage_TraversalOutsidePluginPath_ReturnsNotFound(string imagePath)
    {
        var id = Guid.NewGuid();
        var version = new Version(1, 0);
        SetupPlugin(id, version, imagePath);

        var result = CreateController().GetPluginImage(id, version);

        Assert.IsType<NotFoundResult>(result);
    }

    [Fact]
    public void GetPluginImage_SiblingPrefixDirectory_ReturnsNotFound()
    {
        var id = Guid.NewGuid();
        var version = new Version(1, 0);
        // Resolves to "<pluginPath>-evil/logo.png", which shares the plugin path as a string prefix.
        // The file is created so the check fails on the boundary, not on File.Exists.
        var siblingDir = _pluginPath + "-evil";
        Directory.CreateDirectory(siblingDir);
        File.WriteAllBytes(Path.Combine(siblingDir, "logo.png"), Array.Empty<byte>());
        SetupPlugin(id, version, "../jellyfin-plugin-image-tests-evil/logo.png");

        var result = CreateController().GetPluginImage(id, version);

        Assert.IsType<NotFoundResult>(result);
    }

    [Fact]
    public void GetPluginImage_AbsoluteImagePath_ReturnsNotFound()
    {
        var id = Guid.NewGuid();
        var version = new Version(1, 0);
        SetupPlugin(id, version, OperatingSystem.IsWindows() ? "C:\\Windows\\win.ini" : "/etc/passwd");

        var result = CreateController().GetPluginImage(id, version);

        Assert.IsType<NotFoundResult>(result);
    }

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    public void GetPluginImage_NoImagePathOrResource_ReturnsNotFound(string? imagePath)
    {
        var id = Guid.NewGuid();
        var version = new Version(1, 0);
        SetupPlugin(id, version, imagePath);

        var result = CreateController().GetPluginImage(id, version);

        Assert.IsType<NotFoundResult>(result);
    }
}