Add path validation to prevent path traversal attacks

Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-10-07 14:04:00 +00:00
parent bfd29fc0fc
commit a031e6f5f7
8 changed files with 261 additions and 6 deletions

View File

@@ -2,6 +2,7 @@
using mRemoteNG.Config.DataProviders;
using mRemoteNGTests.TestHelpers;
using NUnit.Framework;
using System;
namespace mRemoteNGTests.Config.DataProviders;
@@ -46,4 +47,18 @@ public class FileBackupCreatorTests
var backupFileExists = File.Exists(_testFilePathBackup);
Assert.That(backupFileExists, Is.False);
}
[Test]
public void CreateBackupFile_WithPathTraversal_ThrowsArgumentException()
{
string maliciousPath = @"..\..\..\Windows\System32\config.xml";
Assert.Throws<ArgumentException>(() => _fileBackupCreator.CreateBackupFile(maliciousPath));
}
[Test]
public void CreateBackupFile_WithForwardSlashTraversal_ThrowsArgumentException()
{
string maliciousPath = @"../../../etc/passwd";
Assert.Throws<ArgumentException>(() => _fileBackupCreator.CreateBackupFile(maliciousPath));
}
}

View File

@@ -0,0 +1,52 @@
using System;
using System.IO;
using mRemoteNG.Config.DataProviders;
using mRemoteNGTests.TestHelpers;
using NUnit.Framework;
namespace mRemoteNGTests.Config.DataProviders;
public class FileBackupPrunerTests
{
private FileBackupPruner _fileBackupPruner;
private string _testFilePath;
private string _testFileDirectory;
[SetUp]
public void Setup()
{
_testFilePath = FileTestHelpers.NewTempFilePath();
_testFileDirectory = Path.GetDirectoryName(_testFilePath);
_fileBackupPruner = new FileBackupPruner();
}
[TearDown]
public void Teardown()
{
if (Directory.Exists(_testFileDirectory))
Directory.Delete(_testFileDirectory, true);
}
[Test]
public void PruneBackupFiles_WithPathTraversal_ThrowsArgumentException()
{
string maliciousPath = @"..\..\..\Windows\System32\config.xml";
Assert.Throws<ArgumentException>(() => _fileBackupPruner.PruneBackupFiles(maliciousPath, 5));
}
[Test]
public void PruneBackupFiles_WithForwardSlashTraversal_ThrowsArgumentException()
{
string maliciousPath = @"../../../etc/passwd";
Assert.Throws<ArgumentException>(() => _fileBackupPruner.PruneBackupFiles(maliciousPath, 5));
}
[Test]
public void PruneBackupFiles_WithValidPath_DoesNotThrow()
{
// Create the test file
File.WriteAllText(_testFilePath, "test");
Assert.DoesNotThrow(() => _fileBackupPruner.PruneBackupFiles(_testFilePath, 5));
}
}

View File

@@ -56,4 +56,32 @@ public class FileDataProviderTests
_dataProvider.Save("");
Assert.That(File.Exists(fileThatShouldExist), Is.True);
}
[Test]
public void Constructor_WithPathTraversal_ThrowsArgumentException()
{
string maliciousPath = @"C:\Users\..\..\..\Windows\System32\config.xml";
Assert.Throws<ArgumentException>(() => new FileDataProvider(maliciousPath));
}
[Test]
public void FilePath_SetWithPathTraversal_ThrowsArgumentException()
{
string maliciousPath = @"..\..\..\Windows\System32\config.xml";
Assert.Throws<ArgumentException>(() => _dataProvider.FilePath = maliciousPath);
}
[Test]
public void MoveTo_WithPathTraversal_ThrowsArgumentException()
{
string maliciousPath = @"..\..\..\Windows\System32\config.xml";
// The method catches the exception internally, so we need to verify it doesn't move the file
_dataProvider.Save("test");
_dataProvider.MoveTo(maliciousPath);
// Verify the file wasn't moved to the malicious path
Assert.That(File.Exists(maliciousPath), Is.False);
// Verify the original file still exists
Assert.That(File.Exists(_testFilePath), Is.True);
}
}

View File

@@ -0,0 +1,91 @@
using System;
using mRemoteNG.Tools;
using NUnit.Framework;
namespace mRemoteNGTests.Tools;
public class PathValidatorTests
{
[Test]
public void ValidPath_ReturnsTrue()
{
string validPath = @"C:\Users\TestUser\Documents\test.xml";
Assert.That(PathValidator.IsValidPath(validPath), Is.True);
}
[Test]
public void PathWithForwardSlashTraversal_ReturnsFalse()
{
string maliciousPath = @"C:\Users\TestUser\Documents\..\..\..\Windows\System32\test.xml";
Assert.That(PathValidator.IsValidPath(maliciousPath), Is.False);
}
[Test]
public void PathWithBackslashTraversal_ReturnsFalse()
{
string maliciousPath = @"C:\Users\TestUser\Documents\..\..\test.xml";
Assert.That(PathValidator.IsValidPath(maliciousPath), Is.False);
}
[Test]
public void PathWithMixedTraversal_ReturnsFalse()
{
string maliciousPath = @"C:\Users\TestUser\Documents\.././..\test.xml";
Assert.That(PathValidator.IsValidPath(maliciousPath), Is.False);
}
[Test]
public void PathWithEncodedTraversal_ReturnsFalse()
{
string maliciousPath = @"C:\Users\TestUser\Documents\%2e%2e\test.xml";
Assert.That(PathValidator.IsValidPath(maliciousPath), Is.False);
}
[Test]
public void PathWithUppercaseEncodedTraversal_ReturnsFalse()
{
string maliciousPath = @"C:\Users\TestUser\Documents\%2E%2E\test.xml";
Assert.That(PathValidator.IsValidPath(maliciousPath), Is.False);
}
[Test]
public void NullPath_ReturnsFalse()
{
Assert.That(PathValidator.IsValidPath(null), Is.False);
}
[Test]
public void EmptyPath_ReturnsFalse()
{
Assert.That(PathValidator.IsValidPath(""), Is.False);
}
[Test]
public void ValidatePathOrThrow_WithValidPath_DoesNotThrow()
{
string validPath = @"C:\Users\TestUser\Documents\test.xml";
Assert.DoesNotThrow(() => PathValidator.ValidatePathOrThrow(validPath));
}
[Test]
public void ValidatePathOrThrow_WithTraversalPath_ThrowsArgumentException()
{
string maliciousPath = @"C:\Users\TestUser\Documents\..\..\..\test.xml";
var exception = Assert.Throws<ArgumentException>(() => PathValidator.ValidatePathOrThrow(maliciousPath));
Assert.That(exception.Message, Does.Contain("path traversal"));
}
[Test]
public void ValidatePathOrThrow_WithNullPath_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => PathValidator.ValidatePathOrThrow(null));
}
[Test]
public void ValidatePathOrThrow_WithCustomParameterName_IncludesParameterName()
{
string maliciousPath = @"..\..\..\test.xml";
var exception = Assert.Throws<ArgumentException>(() => PathValidator.ValidatePathOrThrow(maliciousPath, "customParam"));
Assert.That(exception.ParamName, Is.EqualTo("customParam"));
}
}