Files
ssh.net/test/Renci.SshNet.Tests/Classes/ScpClientTest_EnsureValidLocalName.cs
Nadav0077 600be0de54 Reject unsafe server-supplied names in SCP recursive download
A malicious or compromised SCP server could return file or directory names containing
path separators, drive qualifiers, or parent-directory references.
ScpClient.Download(string, DirectoryInfo) combined these into a local path without
validation, allowing writes outside the destination directory. Server-supplied C and D
record names are now validated before being combined into a local path.

Signed-off-by: Nadav0077 <18245584+Nadav0077@users.noreply.github.com>
2026-08-09 18:32:00 +02:00

103 lines
4.1 KiB
C#

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Renci.SshNet.Common;
namespace Renci.SshNet.Tests.Classes
{
/// <summary>
/// Tests for <see cref="ScpClient.EnsureValidLocalName(string)"/>, which guards the recursive
/// download against server-supplied SCP file/directory names that would write outside the
/// caller-supplied destination directory.
/// </summary>
[TestClass]
public class ScpClientTest_EnsureValidLocalName
{
[TestMethod]
public void PlainFileName_DoesNotThrow()
{
ScpClient.EnsureValidLocalName("owned.txt");
ScpClient.EnsureValidLocalName("2024-report.tar.gz");
ScpClient.EnsureValidLocalName("file with spaces.dat");
ScpClient.EnsureValidLocalName("...");
}
[TestMethod]
public void UnicodeFileName_DoesNotThrow()
{
ScpClient.EnsureValidLocalName("файл.txt");
}
[TestMethod]
public void Empty_ThrowsScpException()
{
_ = Assert.ThrowsExactly<ScpException>(() => ScpClient.EnsureValidLocalName(string.Empty));
}
[TestMethod]
public void CurrentDirectory_ThrowsScpException()
{
_ = Assert.ThrowsExactly<ScpException>(() => ScpClient.EnsureValidLocalName("."));
}
[TestMethod]
public void ParentDirectory_ThrowsScpException()
{
_ = Assert.ThrowsExactly<ScpException>(() => ScpClient.EnsureValidLocalName(".."));
}
[TestMethod]
public void ForwardSlashPath_ThrowsScpException()
{
// '/' is an invalid file name character on every platform.
_ = Assert.ThrowsExactly<ScpException>(() => ScpClient.EnsureValidLocalName("sub/child"));
_ = Assert.ThrowsExactly<ScpException>(() => ScpClient.EnsureValidLocalName("../escaped/owned.txt"));
}
[TestMethod]
public void RootedUnixPath_ThrowsScpException()
{
// Contains '/', so it is rejected regardless of platform.
_ = Assert.ThrowsExactly<ScpException>(() => ScpClient.EnsureValidLocalName("/tmp/sshnet-owned.txt"));
}
[TestMethod]
public void NullCharacter_ThrowsScpException()
{
// NUL is an invalid file name character on every platform.
_ = Assert.ThrowsExactly<ScpException>(() => ScpClient.EnsureValidLocalName("safe\0evil"));
}
[TestMethod]
[OSCondition(OperatingSystems.Windows, IgnoreMessage = "'\\' is only a path separator (and invalid file name char) on Windows.")]
public void BackslashPath_OnWindows_ThrowsScpException()
{
_ = Assert.ThrowsExactly<ScpException>(() => ScpClient.EnsureValidLocalName("..\\escaped\\owned.txt"));
_ = Assert.ThrowsExactly<ScpException>(() => ScpClient.EnsureValidLocalName("sub\\child"));
}
[TestMethod]
[OSCondition(OperatingSystems.Windows, IgnoreMessage = "':' is only an invalid file name char on Windows.")]
public void DriveQualifiedPath_OnWindows_ThrowsScpException()
{
_ = Assert.ThrowsExactly<ScpException>(() => ScpClient.EnsureValidLocalName("C:\\Windows\\System32\\evil.dll"));
}
[TestMethod]
[OSCondition(OperatingSystems.Windows, IgnoreMessage = "':' is only an invalid file name char on Windows.")]
public void AlternateDataStreamName_OnWindows_ThrowsScpException()
{
// NTFS alternate data stream syntax: writes a hidden stream of "safe.txt".
_ = Assert.ThrowsExactly<ScpException>(() => ScpClient.EnsureValidLocalName("safe.txt:evil"));
}
[TestMethod]
[OSCondition(ConditionMode.Exclude, OperatingSystems.Windows, IgnoreMessage = "'\\' is a valid file name byte only on Unix-like platforms.")]
public void BackslashName_OnUnix_DoesNotThrow()
{
// On Unix, '\' is an ordinary file name byte and cannot traverse directories,
// so a name containing it must remain accepted (no behaviour change).
ScpClient.EnsureValidLocalName("name\\with\\backslashes.txt");
}
}
}