mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-07 16:19:07 +00:00
Board completion/media uploads now check domain state first, use server-controlled filenames with an allowlist, store under App_Data, and serve via authenticated download endpoints. Also harden WeekRolled log arguments against CodeQL log-injection findings.
88 lines
2.9 KiB
C#
88 lines
2.9 KiB
C#
using Api.SeaHavenIndustries.Helper;
|
|
using FluentAssertions;
|
|
using Microsoft.AspNetCore.Hosting;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Moq;
|
|
using SeaHaven.Services.Helpers;
|
|
using Xunit;
|
|
|
|
namespace Api.SeaHavenIndustries.Tests;
|
|
|
|
public class WorkOrderPrivateFileStorageTests
|
|
{
|
|
private static (WorkOrderPrivateFileStorage Storage, string Root) CreateStorage()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "wo-files-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(root);
|
|
var env = new Mock<IWebHostEnvironment>();
|
|
env.Setup(e => e.ContentRootPath).Returns(root);
|
|
return (new WorkOrderPrivateFileStorage(env.Object), root);
|
|
}
|
|
|
|
private static IFormFile FakeFile(string fileName, byte[]? bytes = null)
|
|
{
|
|
bytes ??= new byte[] { 1, 2, 3, 4 };
|
|
var stream = new MemoryStream(bytes);
|
|
return new FormFile(stream, 0, bytes.Length, "file", fileName)
|
|
{
|
|
Headers = new HeaderDictionary(),
|
|
ContentType = "application/octet-stream"
|
|
};
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SaveAsync_UsesServerControlledName_OutsideWwwroot()
|
|
{
|
|
var (storage, root) = CreateStorage();
|
|
try
|
|
{
|
|
var key = await storage.SaveAsync(42, FakeFile("../../evil.pdf"));
|
|
key.Should().StartWith("workorder-files/42/");
|
|
key.Should().EndWith(".pdf");
|
|
key.Should().NotContain("..");
|
|
key.Should().NotContain("evil");
|
|
|
|
storage.TryResolvePhysicalPath(key, out var physical).Should().BeTrue();
|
|
physical.Should().StartWith(Path.Combine(root, "App_Data", "workorder-files"));
|
|
File.Exists(physical).Should().BeTrue();
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SaveAsync_RejectsDisallowedExtension()
|
|
{
|
|
var (storage, root) = CreateStorage();
|
|
try
|
|
{
|
|
var act = async () => await storage.SaveAsync(1, FakeFile("payload.exe"));
|
|
await act.Should().ThrowAsync<InvalidOperationException>();
|
|
Directory.Exists(Path.Combine(root, "App_Data", "workorder-files", "1")).Should().BeFalse();
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void TryResolvePhysicalPath_RejectsPathEscape()
|
|
{
|
|
var (storage, root) = CreateStorage();
|
|
try
|
|
{
|
|
storage.TryResolvePhysicalPath("workorder-files/../secret.txt", out _).Should().BeFalse();
|
|
storage.TryResolvePhysicalPath("https://host/Assets/Documents/x.pdf", out _).Should().BeFalse();
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
}
|