From f679a783bfdc6f37db6df2885d900e3ff687cf68 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Date: Mon, 20 Jul 2026 12:38:11 +0100
Subject: [PATCH 1/2] .Net: harden FileIOPlugin UNC path rejection to cover all
separator forms
Align FileIOPlugin path validation with the sibling file plugins by rejecting any path that begins with two directory separators, in any combination of backslash and forward slash. Previously only the uniform backslash form was rejected, so forward slash and mixed separator forms could reach path canonicalization before the allowed folder check ran. Adds a post canonicalization re check and unit tests covering all separator forms for read and write.
---
.../src/Plugins/Plugins.Core/FileIOPlugin.cs | 24 +++++++++++++-
.../Core/FileIOPluginTests.cs | 32 +++++++++++++++++++
2 files changed, 55 insertions(+), 1 deletion(-)
diff --git a/dotnet/src/Plugins/Plugins.Core/FileIOPlugin.cs b/dotnet/src/Plugins/Plugins.Core/FileIOPlugin.cs
index c67b8a9ad089..c83d2cc0e6f8 100644
--- a/dotnet/src/Plugins/Plugins.Core/FileIOPlugin.cs
+++ b/dotnet/src/Plugins/Plugins.Core/FileIOPlugin.cs
@@ -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));
}
@@ -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.
@@ -162,5 +169,20 @@ private bool TryGetAllowedFilePath(string path, out string canonicalPath)
return false;
}
+
+ ///
+ /// Determines whether the provided path is a UNC path (e.g., \\server\share or
+ /// //server/share) or an extended-length / device path (e.g., \\?\, \\.\).
+ /// Windows treats any two leading directory separators, in any combination of \ and
+ /// /, as such a root, so all combinations are rejected for consistency with the other
+ /// file plugins.
+ ///
+ private static bool IsUncOrExtendedPath(string path)
+ {
+ return path.Length >= 2 && IsDirectorySeparator(path[0]) && IsDirectorySeparator(path[1]);
+ }
+
+ private static bool IsDirectorySeparator(char c)
+ => c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar;
#endregion
}
diff --git a/dotnet/src/Plugins/Plugins.UnitTests/Core/FileIOPluginTests.cs b/dotnet/src/Plugins/Plugins.UnitTests/Core/FileIOPluginTests.cs
index 29d5ba6c5d1d..f6f8fd82880d 100644
--- a/dotnet/src/Plugins/Plugins.UnitTests/Core/FileIOPluginTests.cs
+++ b/dotnet/src/Plugins/Plugins.UnitTests/Core/FileIOPluginTests.cs
@@ -144,6 +144,38 @@ public async Task ItCannotReadFromDisallowedFoldersAsync()
await Assert.ThrowsAsync(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 & Assert
+ await Assert.ThrowsAsync(() => plugin.ReadAsync(uncPath));
+ }
+
+ [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 & Assert
+ await Assert.ThrowsAsync(() => plugin.WriteAsync(uncPath, "hello world"));
+ }
+
[Fact]
public async Task ItCannotReadThroughSymlinkOutsideAllowedFoldersAsync()
{
From 4dddc48b1199e1b377636d0d8e26a99472623afc Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Date: Mon, 20 Jul 2026 12:46:14 +0100
Subject: [PATCH 2/2] Address review: make separator check OS-independent and
assert UNC guard message
Check for backslash and forward slash characters explicitly instead of relying on Path.DirectorySeparatorChar, which resolves to the same character on non-Windows and would miss backslash forms there. Strengthen the UNC tests to assert the rejection message so they fail if a different validation path is what throws.
---
dotnet/src/Plugins/Plugins.Core/FileIOPlugin.cs | 6 +++---
.../Plugins.UnitTests/Core/FileIOPluginTests.cs | 14 ++++++++++----
2 files changed, 13 insertions(+), 7 deletions(-)
diff --git a/dotnet/src/Plugins/Plugins.Core/FileIOPlugin.cs b/dotnet/src/Plugins/Plugins.Core/FileIOPlugin.cs
index c83d2cc0e6f8..c7106e76336a 100644
--- a/dotnet/src/Plugins/Plugins.Core/FileIOPlugin.cs
+++ b/dotnet/src/Plugins/Plugins.Core/FileIOPlugin.cs
@@ -175,14 +175,14 @@ private bool TryGetAllowedFilePath(string path, out string canonicalPath)
/// //server/share) or an extended-length / device path (e.g., \\?\, \\.\).
/// Windows treats any two leading directory separators, in any combination of \ and
/// /, as such a root, so all combinations are rejected for consistency with the other
- /// file plugins.
+ /// file plugins. Both separator characters are checked explicitly so the result does not
+ /// depend on the current operating system's separator.
///
private static bool IsUncOrExtendedPath(string path)
{
return path.Length >= 2 && IsDirectorySeparator(path[0]) && IsDirectorySeparator(path[1]);
}
- private static bool IsDirectorySeparator(char c)
- => c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar;
+ private static bool IsDirectorySeparator(char c) => c is '\\' or '/';
#endregion
}
diff --git a/dotnet/src/Plugins/Plugins.UnitTests/Core/FileIOPluginTests.cs b/dotnet/src/Plugins/Plugins.UnitTests/Core/FileIOPluginTests.cs
index f6f8fd82880d..d28cffb2aa43 100644
--- a/dotnet/src/Plugins/Plugins.UnitTests/Core/FileIOPluginTests.cs
+++ b/dotnet/src/Plugins/Plugins.UnitTests/Core/FileIOPluginTests.cs
@@ -154,8 +154,11 @@ public async Task ItCannotReadFromUncPathsAsync(string uncPath)
// Arrange
var plugin = new FileIOPlugin() { AllowedFolders = [Path.GetTempPath()] };
- // Act & Assert
- await Assert.ThrowsAsync(() => plugin.ReadAsync(uncPath));
+ // Act
+ var exception = await Assert.ThrowsAsync(() => 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]
@@ -172,8 +175,11 @@ public async Task ItCannotWriteToUncPathsAsync(string uncPath)
DisableFileOverwrite = false
};
- // Act & Assert
- await Assert.ThrowsAsync(() => plugin.WriteAsync(uncPath, "hello world"));
+ // Act
+ var exception = await Assert.ThrowsAsync(() => 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]