diff --git a/dotnet/src/Plugins/Plugins.Core/FileIOPlugin.cs b/dotnet/src/Plugins/Plugins.Core/FileIOPlugin.cs
index c67b8a9ad089..c7106e76336a 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. 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 is '\\' or '/';
#endregion
}
diff --git a/dotnet/src/Plugins/Plugins.UnitTests/Core/FileIOPluginTests.cs b/dotnet/src/Plugins/Plugins.UnitTests/Core/FileIOPluginTests.cs
index 29d5ba6c5d1d..d28cffb2aa43 100644
--- a/dotnet/src/Plugins/Plugins.UnitTests/Core/FileIOPluginTests.cs
+++ b/dotnet/src/Plugins/Plugins.UnitTests/Core/FileIOPluginTests.cs
@@ -144,6 +144,44 @@ 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
+ 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]
+ [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(() => 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()
{