From 00bcde411c16c9d59d5ba60013839ed24227cdb7 Mon Sep 17 00:00:00 2001 From: King Star Date: Sat, 18 Jul 2026 17:57:50 +0800 Subject: [PATCH 1/2] fix: reject mixed-separator UNC paths Signed-off-by: King Star --- .../src/Plugins/Plugins.Core/FileIOPlugin.cs | 7 +++- .../Core/FileIOPluginTests.cs | 35 +++++++++++++++++++ .../Web/WebFileDownloadPluginTests.cs | 20 +++++++++++ .../Plugins.Web/WebFileDownloadPlugin.cs | 11 +++--- 4 files changed, 66 insertions(+), 7 deletions(-) diff --git a/dotnet/src/Plugins/Plugins.Core/FileIOPlugin.cs b/dotnet/src/Plugins/Plugins.Core/FileIOPlugin.cs index c67b8a9ad089..afe42a0e2665 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)); } @@ -162,5 +162,10 @@ private bool TryGetAllowedFilePath(string path, out string canonicalPath) return false; } + + private static bool IsUncOrExtendedPath(string path) => + path.Length >= 2 && + (path[0] is '/' or '\\') && + (path[1] 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..0b309f1dfb0d 100644 --- a/dotnet/src/Plugins/Plugins.UnitTests/Core/FileIOPluginTests.cs +++ b/dotnet/src/Plugins/Plugins.UnitTests/Core/FileIOPluginTests.cs @@ -144,6 +144,41 @@ public async Task ItCannotReadFromDisallowedFoldersAsync() await Assert.ThrowsAsync(async () => await plugin.ReadAsync(Path.Combine("", Path.GetRandomFileName()))); } + [Theory] + [InlineData("\\\\UNC\\server\\folder\\myfile.txt")] + [InlineData("//UNC/server/folder/myfile.txt")] + [InlineData("/\\UNC\\server\\folder\\myfile.txt")] + [InlineData("\\/UNC/server/folder/myfile.txt")] + public async Task ItRejectsUncOrExtendedPathsOnReadAsync(string path) + { + // Arrange + var plugin = new FileIOPlugin() + { + AllowedFolders = [Path.GetTempPath()] + }; + + // Act & Assert + await Assert.ThrowsAsync(() => plugin.ReadAsync(path)); + } + + [Theory] + [InlineData("\\\\UNC\\server\\folder\\myfile.txt")] + [InlineData("//UNC/server/folder/myfile.txt")] + [InlineData("/\\UNC\\server\\folder\\myfile.txt")] + [InlineData("\\/UNC/server/folder/myfile.txt")] + public async Task ItRejectsUncOrExtendedPathsOnWriteAsync(string path) + { + // Arrange + var plugin = new FileIOPlugin() + { + AllowedFolders = [Path.GetTempPath()], + DisableFileOverwrite = false + }; + + // Act & Assert + await Assert.ThrowsAsync(() => plugin.WriteAsync(path, "hello world")); + } + [Fact] public async Task ItCannotReadThroughSymlinkOutsideAllowedFoldersAsync() { diff --git a/dotnet/src/Plugins/Plugins.UnitTests/Web/WebFileDownloadPluginTests.cs b/dotnet/src/Plugins/Plugins.UnitTests/Web/WebFileDownloadPluginTests.cs index 92a15be0c1df..dbba79540f4d 100644 --- a/dotnet/src/Plugins/Plugins.UnitTests/Web/WebFileDownloadPluginTests.cs +++ b/dotnet/src/Plugins/Plugins.UnitTests/Web/WebFileDownloadPluginTests.cs @@ -230,6 +230,26 @@ public async Task DownloadToFileFailsForInvalidParametersAsync() await Assert.ThrowsAsync(async () => await webFileDownload.DownloadToFileAsync(validUri, "myfile.txt")); } + [Theory] + [InlineData("\\\\UNC\\server\\folder\\myfile.txt")] + [InlineData("//UNC/server/folder/myfile.txt")] + [InlineData("/\\UNC\\server\\folder\\myfile.txt")] + [InlineData("\\/UNC/server/folder/myfile.txt")] + public async Task DownloadToFileRejectsUncOrExtendedPathsAsync(string filePath) + { + // Arrange + var uri = new Uri("https://raw.githubusercontent.com/microsoft/semantic-kernel/refs/heads/main/docs/images/sk_logo.png"); + var folderPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + var webFileDownload = new WebFileDownloadPlugin() + { + AllowedDomains = ["raw.githubusercontent.com"], + AllowedFolders = [folderPath] + }; + + // Act & Assert + await Assert.ThrowsAsync(() => webFileDownload.DownloadToFileAsync(uri, filePath)); + } + [Fact] public async Task DownloadToFileUsesCaseSensitiveAllowListComparisonOnLinuxAsync() { diff --git a/dotnet/src/Plugins/Plugins.Web/WebFileDownloadPlugin.cs b/dotnet/src/Plugins/Plugins.Web/WebFileDownloadPlugin.cs index acdcd9659fd6..e068f65aeb2a 100644 --- a/dotnet/src/Plugins/Plugins.Web/WebFileDownloadPlugin.cs +++ b/dotnet/src/Plugins/Plugins.Web/WebFileDownloadPlugin.cs @@ -224,11 +224,10 @@ private static string CanonicalizePath(string path) return PathUtilities.GetSafeFullPath(expanded); } - private static bool IsUncOrExtendedPath(string path) - { - return path.StartsWith("\\\\", StringComparison.OrdinalIgnoreCase) || - path.StartsWith("//", StringComparison.OrdinalIgnoreCase); - } + private static bool IsUncOrExtendedPath(string path) => + path.Length >= 2 && + (path[0] is '/' or '\\') && + (path[1] is '/' or '\\'); /// /// If a list of allowed folder has been provided, the folder of the provided filePath is checked @@ -239,7 +238,7 @@ private bool IsFilePathAllowed(string path) { Verify.NotNullOrWhiteSpace(path); - if (path.StartsWith("\\\\", StringComparison.OrdinalIgnoreCase)) + if (IsUncOrExtendedPath(path)) { throw new ArgumentException("Invalid file path, UNC paths are not supported.", nameof(path)); } From 3b7b9767fdd75accc5f14ee33b127fa5b2a55727 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> Date: Mon, 20 Jul 2026 12:58:25 +0100 Subject: [PATCH 2/2] Reject UNC paths resolved during full-path canonicalization Address review feedback: a relative path can still resolve to a UNC path when the current directory is a UNC share. Resolve the full path with Path.GetFullPath first (no filesystem access) and re-check for UNC or extended-path prefixes before GetSafeFullPath probes the filesystem while resolving symbolic links. --- dotnet/src/Plugins/Plugins.Core/FileIOPlugin.cs | 12 +++++++++++- .../src/Plugins/Plugins.Web/WebFileDownloadPlugin.cs | 12 +++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Plugins/Plugins.Core/FileIOPlugin.cs b/dotnet/src/Plugins/Plugins.Core/FileIOPlugin.cs index afe42a0e2665..5d11b4b023e7 100644 --- a/dotnet/src/Plugins/Plugins.Core/FileIOPlugin.cs +++ b/dotnet/src/Plugins/Plugins.Core/FileIOPlugin.cs @@ -125,7 +125,17 @@ private bool TryGetAllowedFilePath(string path, out string canonicalPath) throw new ArgumentException("Invalid file path, a fully qualified file location must be specified.", nameof(path)); } - canonicalPath = PathUtilities.GetSafeFullPath(path); + // Resolve the full path first (a pure string operation that does not touch the + // filesystem). A relative path can still resolve to a UNC path here, for example + // when the current directory is a UNC share, so re-check before GetSafeFullPath + // probes the filesystem while resolving symbolic links. + canonicalPath = Path.GetFullPath(path); + if (IsUncOrExtendedPath(canonicalPath)) + { + throw new ArgumentException("Invalid file path, UNC paths are not supported.", nameof(path)); + } + + canonicalPath = PathUtilities.GetSafeFullPath(canonicalPath); if (File.Exists(canonicalPath) && File.GetAttributes(canonicalPath).HasFlag(FileAttributes.ReadOnly)) { diff --git a/dotnet/src/Plugins/Plugins.Web/WebFileDownloadPlugin.cs b/dotnet/src/Plugins/Plugins.Web/WebFileDownloadPlugin.cs index e068f65aeb2a..f08482c0d124 100644 --- a/dotnet/src/Plugins/Plugins.Web/WebFileDownloadPlugin.cs +++ b/dotnet/src/Plugins/Plugins.Web/WebFileDownloadPlugin.cs @@ -221,7 +221,17 @@ private static string CanonicalizePath(string path) throw new ArgumentException("Invalid file path, UNC paths are not supported.", nameof(path)); } - return PathUtilities.GetSafeFullPath(expanded); + // Resolve the full path first (a pure string operation that does not touch the + // filesystem). A relative path can still resolve to a UNC path here, for example + // when the current directory is a UNC share, so re-check before GetSafeFullPath + // probes the filesystem while resolving symbolic links. + var fullPath = Path.GetFullPath(expanded); + if (IsUncOrExtendedPath(fullPath)) + { + throw new ArgumentException("Invalid file path, UNC paths are not supported.", nameof(path)); + } + + return PathUtilities.GetSafeFullPath(fullPath); } private static bool IsUncOrExtendedPath(string path) =>