Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion dotnet/src/Plugins/Plugins.Core/FileIOPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ private bool TryGetAllowedFilePath(string path, out string canonicalPath)
Verify.NotNullOrWhiteSpace(path);
canonicalPath = string.Empty;

if (path.StartsWith("\\\\", StringComparison.OrdinalIgnoreCase))
if (IsUncOrExtendedPath(path))
{
throw new ArgumentException("Invalid file path, UNC paths are not supported.", nameof(path));
}
Expand All @@ -127,6 +127,13 @@ private bool TryGetAllowedFilePath(string path, out string canonicalPath)

canonicalPath = PathUtilities.GetSafeFullPath(path);

// Re-check after canonicalization: resolving the path could produce a UNC
// or extended-path prefix that was not present in the original input.
if (IsUncOrExtendedPath(canonicalPath))
{
throw new ArgumentException("Invalid file path, UNC paths are not supported.", nameof(path));
}

if (File.Exists(canonicalPath) && File.GetAttributes(canonicalPath).HasFlag(FileAttributes.ReadOnly))
{
// Most environments will throw this with OpenWrite, but running inside docker on Linux will not.
Expand Down Expand Up @@ -162,5 +169,20 @@ private bool TryGetAllowedFilePath(string path, out string canonicalPath)

return false;
}

/// <summary>
/// Determines whether the provided path is a UNC path (e.g., <c>\\server\share</c> or
/// <c>//server/share</c>) or an extended-length / device path (e.g., <c>\\?\</c>, <c>\\.\</c>).
/// Windows treats any two leading directory separators, in any combination of <c>\</c> and
/// <c>/</c>, as such a root, so all combinations are rejected for consistency with the other
/// file plugins. Both separator characters are checked explicitly so the result does not
/// depend on the current operating system's separator.
/// </summary>
private static bool IsUncOrExtendedPath(string path)
{
return path.Length >= 2 && IsDirectorySeparator(path[0]) && IsDirectorySeparator(path[1]);
}

private static bool IsDirectorySeparator(char c) => c is '\\' or '/';
#endregion
}
38 changes: 38 additions & 0 deletions dotnet/src/Plugins/Plugins.UnitTests/Core/FileIOPluginTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,44 @@ public async Task ItCannotReadFromDisallowedFoldersAsync()
await Assert.ThrowsAsync<ArgumentException>(async () => await plugin.ReadAsync(Path.Combine("", Path.GetRandomFileName())));
}

[Theory]
[InlineData("\\\\server\\share\\file.txt")]
[InlineData("//server/share/file.txt")]
[InlineData("\\/server\\share\\file.txt")]
[InlineData("/\\server/share/file.txt")]
public async Task ItCannotReadFromUncPathsAsync(string uncPath)
{
// Arrange
var plugin = new FileIOPlugin() { AllowedFolders = [Path.GetTempPath()] };

// Act
var exception = await Assert.ThrowsAsync<ArgumentException>(() => plugin.ReadAsync(uncPath));

// Assert - the UNC guard (not another validation) must be what rejected the path
Assert.Contains("UNC paths are not supported", exception.Message, StringComparison.Ordinal);
}

[Theory]
[InlineData("\\\\server\\share\\file.txt")]
[InlineData("//server/share/file.txt")]
[InlineData("\\/server\\share\\file.txt")]
[InlineData("/\\server/share/file.txt")]
public async Task ItCannotWriteToUncPathsAsync(string uncPath)
{
// Arrange
var plugin = new FileIOPlugin()
{
AllowedFolders = [Path.GetTempPath()],
DisableFileOverwrite = false
};

// Act
var exception = await Assert.ThrowsAsync<ArgumentException>(() => plugin.WriteAsync(uncPath, "hello world"));

// Assert - the UNC guard (not another validation) must be what rejected the path
Assert.Contains("UNC paths are not supported", exception.Message, StringComparison.Ordinal);
}

[Fact]
public async Task ItCannotReadThroughSymlinkOutsideAllowedFoldersAsync()
{
Expand Down
Loading