Public Access
- A remote name went straight into Path.Combine. A POSIX name may hold backslashes, a colon or a drive letter, and an S3 key may hold "../", so a name chosen by whoever could write to the host decided where on this machine a download landed. LocalDirectory.ChildPath cleans the name and requires the resolved parent to be the target folder; every download path and EDIT use it, and a name that cannot be a file there is left on the host and reported. - EDIT handed the copy to the shell's default action, which for .bat, .hta, .js and friends is to run it. LocalEditor opens such a file with its own "edit" verb or Notepad on Windows and `open -t` on macOS, and refuses a .desktop launcher on Linux. - A failed bucket upload leaves its part object, and a retry asked for an offset no bucket accepts, so the row could never finish. IRemoteFileStore.CanWriteFromAnOffset is false for buckets and the queue starts those over. - The SFTP host picker re-found its selection from the empty moment of every vault reload and landed on the first host once a minute; the chosen host is now remembered, and a typed password is only sent to the host it was typed for.
69 lines
2.5 KiB
C#
69 lines
2.5 KiB
C#
namespace DodoSSH.Client.Transfer.Tests;
|
|
|
|
/// <summary>
|
|
/// <c>LocalDirectory.ChildPath</c>: where a name another machine chose may land on this one.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The assertion that matters is the same in every row — the result's parent is the directory asked about,
|
|
/// or there is no result — because that is the property a download relies on, and it is checked against the
|
|
/// resolved path rather than the string so that what the file system does to a name counts too.
|
|
/// </remarks>
|
|
public sealed class LocalDirectoryTests
|
|
{
|
|
private static readonly string Directory = Path.Combine(Path.GetTempPath(), "dodossh-child-path");
|
|
|
|
[Theory]
|
|
[InlineData("notes.txt")]
|
|
[InlineData("backup:2026-10-02")]
|
|
[InlineData(@"..\..\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\x.bat")]
|
|
[InlineData(@"C:\Windows\evil.dll")]
|
|
[InlineData("../../escaped.txt")]
|
|
[InlineData("/etc/passwd")]
|
|
[InlineData("...")]
|
|
[InlineData(".hidden")]
|
|
public void ANameThatCanBeAFileHere_LandsDirectlyInsideTheDirectory(string name)
|
|
{
|
|
var child = LocalDirectory.ChildPath(Directory, name);
|
|
|
|
if (child is null)
|
|
{
|
|
// Only acceptable for "...", which Windows reduces to nothing; everywhere else it is a name.
|
|
OperatingSystem.IsWindows().ShouldBeTrue($"'{name}' was refused");
|
|
name.ShouldBe("...");
|
|
return;
|
|
}
|
|
|
|
Path.GetDirectoryName(child).ShouldBe(Path.GetFullPath(Directory));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("")]
|
|
[InlineData(" ")]
|
|
[InlineData(".")]
|
|
[InlineData("..")]
|
|
public void ANameThatIsOnlyHereOrAbove_IsRefused(string name) =>
|
|
LocalDirectory.ChildPath(Directory, name).ShouldBeNull();
|
|
|
|
/// <summary>
|
|
/// Windows drops a trailing dot or space from a path component, so <c>.. </c> is <c>..</c> by the time
|
|
/// it reaches the disk — which a check of the string alone would have let through.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ATrailingSpaceOrDot_DoesNotSmuggleAParentReferencePastTheCheck()
|
|
{
|
|
foreach (var name in new[] { ".. ", "...", ".. .", ". " })
|
|
{
|
|
var child = LocalDirectory.ChildPath(Directory, name);
|
|
|
|
if (child is not null)
|
|
{
|
|
Path.GetDirectoryName(child).ShouldBe(Path.GetFullPath(Directory), name);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void AnOrdinaryName_IsKeptAsItIs() =>
|
|
Path.GetFileName(LocalDirectory.ChildPath(Directory, "nginx.conf")).ShouldBe("nginx.conf");
|
|
}
|