From 6e378f289870e2dfd84fc6f3bd940bcadaae7cef Mon Sep 17 00:00:00 2001 From: Jeremy Schoemaker Date: Sun, 23 Aug 2026 11:40:04 -0500 Subject: [PATCH] .Net: Reject mixed-separator UNC paths in DocumentPlugin PR #14166 hardened the UNC path check in FileIOPlugin and WebFileDownloadPlugin to reject mixed-separator forms such as \/server/share and /\server\share, and to re-check the resolved full path before symlink resolution. DocumentPlugin has the same CanonicalizePath helper but was not included in that change, so it still accepts mixed-separator UNC paths. Apply the identical hardening to DocumentPlugin and port the mixed-separator test cases to DocumentPluginTests. --- .../Plugins.Document/DocumentPlugin.cs | 21 +++++++++++++------ .../Document/DocumentPluginTests.cs | 20 ++++++++++++++++++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/dotnet/src/Plugins/Plugins.Document/DocumentPlugin.cs b/dotnet/src/Plugins/Plugins.Document/DocumentPlugin.cs index 906668f261e3..7df2812a8a70 100644 --- a/dotnet/src/Plugins/Plugins.Document/DocumentPlugin.cs +++ b/dotnet/src/Plugins/Plugins.Document/DocumentPlugin.cs @@ -161,15 +161,24 @@ 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)); + } - private static bool IsUncOrExtendedPath(string path) - { - return path.StartsWith("\\\\", StringComparison.OrdinalIgnoreCase) || - path.StartsWith("//", StringComparison.OrdinalIgnoreCase); + return PathUtilities.GetSafeFullPath(fullPath); } + private static bool IsUncOrExtendedPath(string path) => + path.Length >= 2 && + (path[0] is '/' or '\\') && + (path[1] is '/' or '\\'); + /// /// Checks whether a canonicalized file path falls within one of the allowed directories. /// Subdirectories of allowed directories are also permitted. diff --git a/dotnet/src/Plugins/Plugins.UnitTests/Document/DocumentPluginTests.cs b/dotnet/src/Plugins/Plugins.UnitTests/Document/DocumentPluginTests.cs index f31232f34e71..809abfdc84da 100644 --- a/dotnet/src/Plugins/Plugins.UnitTests/Document/DocumentPluginTests.cs +++ b/dotnet/src/Plugins/Plugins.UnitTests/Document/DocumentPluginTests.cs @@ -192,6 +192,26 @@ public async Task ItDeniesUncPathsAsync() await Assert.ThrowsAnyAsync(async () => await target.AppendTextAsync("text", "//UNC/server/folder/file.docx")); } + [Theory] + [InlineData("\\\\UNC\\server\\folder\\myfile.docx")] + [InlineData("//UNC/server/folder/myfile.docx")] + [InlineData("/\\UNC\\server\\folder\\myfile.docx")] + [InlineData("\\/UNC/server/folder/myfile.docx")] + public async Task ItRejectsUncOrExtendedPathsAsync(string path) + { + // Arrange + var fileSystemConnectorMock = new Mock(); + var documentConnectorMock = new Mock(); + var target = new DocumentPlugin(documentConnectorMock.Object, fileSystemConnectorMock.Object) + { + AllowedDirectories = [Path.GetTempPath()] + }; + + // Act & Assert + await Assert.ThrowsAsync(() => target.ReadTextAsync(path)); + await Assert.ThrowsAsync(() => target.AppendTextAsync("text", path)); + } + [Fact] public async Task ItDeniesDisallowedFoldersAsync() {