From a00824340d419be3b82ec445f3fbe7ab39d59002 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Fri, 23 Oct 2026 14:13:43 +0200 Subject: [PATCH 1/3] fix(dotnet): reject session upload dot segments Reject standalone . and .. path segments in model-supplied remote upload names before local file or network I/O, matching the existing Python protection. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../CodeInterpreter/SessionsPythonPlugin.cs | 13 +++++- .../Core/SessionsPythonPluginTests.cs | 46 +++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Plugins/Plugins.Core/CodeInterpreter/SessionsPythonPlugin.cs b/dotnet/src/Plugins/Plugins.Core/CodeInterpreter/SessionsPythonPlugin.cs index 9433de2a86bb..28f7f3a04293 100644 --- a/dotnet/src/Plugins/Plugins.Core/CodeInterpreter/SessionsPythonPlugin.cs +++ b/dotnet/src/Plugins/Plugins.Core/CodeInterpreter/SessionsPythonPlugin.cs @@ -111,7 +111,7 @@ public async Task ExecuteCodeAsync( /// /// Uploads a file to the `/mnt/data` directory of the current session. /// - /// The name of the remote file, relative to `/mnt/data`. + /// The name of the remote file, relative to `/mnt/data`. Must not contain `.` or `..` path segments. /// The path to the file on the local machine. /// The cancellation token. /// The metadata of the uploaded file. @@ -120,13 +120,14 @@ public async Task ExecuteCodeAsync( /// Thrown when file operations are disabled or the path is not allowed. [KernelFunction, Description("Uploads a file to the `/mnt/data` directory of the current session.")] public async Task UploadFileAsync( - [Description("The name of the remote file, relative to `/mnt/data`.")] string remoteFileName, + [Description("The name of the remote file, relative to `/mnt/data`. Must not contain `.` or `..` path segments.")] string remoteFileName, [Description("The path to the file on the local machine.")] string localFilePath, CancellationToken cancellationToken = default) { Verify.NotNullOrWhiteSpace(remoteFileName, nameof(remoteFileName)); Verify.NotNullOrWhiteSpace(localFilePath, nameof(localFilePath)); + ValidateRemoteFileName(remoteFileName); var validatedLocalPath = this.ValidateLocalPathForUpload(localFilePath); this._logger.LogInformation("Uploading file: {LocalFilePath} to {RemoteFileName}", validatedLocalPath, remoteFileName); @@ -292,6 +293,14 @@ private async Task SendAsync(HttpClient httpClient, HttpMet return await httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false); } + private static void ValidateRemoteFileName(string remoteFileName) + { + if (remoteFileName.Split('/').Any(segment => segment is "." or "..")) + { + throw new InvalidOperationException("The remote file name must not contain '.' or '..' path segments."); + } + } + /// /// Validates that the local file path is within allowed upload directories. /// diff --git a/dotnet/src/Plugins/Plugins.UnitTests/Core/SessionsPythonPluginTests.cs b/dotnet/src/Plugins/Plugins.UnitTests/Core/SessionsPythonPluginTests.cs index 7899f6fb9c2d..20080f6792e3 100644 --- a/dotnet/src/Plugins/Plugins.UnitTests/Core/SessionsPythonPluginTests.cs +++ b/dotnet/src/Plugins/Plugins.UnitTests/Core/SessionsPythonPluginTests.cs @@ -278,6 +278,52 @@ public async Task ItShouldUploadFileAsync() Assert.Equal(this._messageHandlerStub.FirstMultipartContent, requestPayload); } + [Theory] + [InlineData(".")] + [InlineData("..")] + [InlineData("../test-file.txt")] + [InlineData("../../test-file.txt")] + [InlineData("./test-file.txt")] + [InlineData("nested/./test-file.txt")] + [InlineData("nested/../test-file.txt")] + [InlineData("nested/..")] + [InlineData("/mnt/data/../../test-file.txt")] + public async Task ItShouldRejectUploadRemoteFileNamesWithDotSegmentsAsync(string remoteFileName) + { + // Arrange + var plugin = new SessionsPythonPlugin(this._settingsWithFileOperationsEnabled, this._httpClientFactory); + + // Act + var exception = await Assert.ThrowsAsync( + () => plugin.UploadFileAsync(remoteFileName, FileTestDataFilePath)); + + // Assert + Assert.Contains("must not contain '.' or '..' path segments", exception.Message); + Assert.Null(this._messageHandlerStub.RequestUri); + } + + [Theory] + [InlineData("nested/dir/test-file.txt")] + [InlineData(".hidden/test-file.txt")] + [InlineData("nested/a..b/test-file.txt")] + public async Task ItShouldAllowUploadRemoteFileNamesWithoutDotSegmentsAsync(string remoteFileName) + { + // Arrange + var responseContent = await File.ReadAllTextAsync(UpdaloadFileTestDataFilePath); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(responseContent), + }; + + var plugin = new SessionsPythonPlugin(this._settingsWithFileOperationsEnabled, this._httpClientFactory); + + // Act + await plugin.UploadFileAsync(remoteFileName, FileTestDataFilePath); + + // Assert + Assert.NotNull(this._messageHandlerStub.RequestUri); + } + [Fact] public async Task ItShouldDownloadFileWithoutSavingInDiskAsync() { From bc0ad0aeb929a550efa72df93c23bf19b2db68de Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Fri, 23 Oct 2026 16:03:33 +0200 Subject: [PATCH 2/3] chore(dotnet): update MessagePack to 3.1.9 Use the first patched 3.x release so NuGet audit no longer fails the .NET build matrix. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- dotnet/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index f4a51a045a48..534116db67a1 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -13,7 +13,7 @@ - + From 43e1c0edc08872ab70d37d7f8e8fe79f70f0e84b Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Fri, 23 Oct 2026 19:23:00 +0200 Subject: [PATCH 3/3] fix(dotnet): reject rooted session upload paths Treat both slash styles as remote path separators and reject rooted, drive-qualified, and UNC upload names before file or network I/O. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../CodeInterpreter/SessionsPythonPlugin.cs | 15 +++++++++++---- .../Core/SessionsPythonPluginTests.cs | 13 +++++++++++-- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/dotnet/src/Plugins/Plugins.Core/CodeInterpreter/SessionsPythonPlugin.cs b/dotnet/src/Plugins/Plugins.Core/CodeInterpreter/SessionsPythonPlugin.cs index 28f7f3a04293..681298cd0dc7 100644 --- a/dotnet/src/Plugins/Plugins.Core/CodeInterpreter/SessionsPythonPlugin.cs +++ b/dotnet/src/Plugins/Plugins.Core/CodeInterpreter/SessionsPythonPlugin.cs @@ -111,7 +111,7 @@ public async Task ExecuteCodeAsync( /// /// Uploads a file to the `/mnt/data` directory of the current session. /// - /// The name of the remote file, relative to `/mnt/data`. Must not contain `.` or `..` path segments. + /// The name of the remote file, relative to `/mnt/data`. Must not be rooted or contain `.` or `..` path segments. /// The path to the file on the local machine. /// The cancellation token. /// The metadata of the uploaded file. @@ -120,7 +120,7 @@ public async Task ExecuteCodeAsync( /// Thrown when file operations are disabled or the path is not allowed. [KernelFunction, Description("Uploads a file to the `/mnt/data` directory of the current session.")] public async Task UploadFileAsync( - [Description("The name of the remote file, relative to `/mnt/data`. Must not contain `.` or `..` path segments.")] string remoteFileName, + [Description("The name of the remote file, relative to `/mnt/data`. Must not be rooted or contain `.` or `..` path segments.")] string remoteFileName, [Description("The path to the file on the local machine.")] string localFilePath, CancellationToken cancellationToken = default) { @@ -295,9 +295,16 @@ private async Task SendAsync(HttpClient httpClient, HttpMet private static void ValidateRemoteFileName(string remoteFileName) { - if (remoteFileName.Split('/').Any(segment => segment is "." or "..")) + var hasDrivePrefix = remoteFileName.Length >= 2 + && remoteFileName[1] == ':' + && char.IsLetter(remoteFileName[0]); + + if (remoteFileName[0] is '/' or '\\' + || hasDrivePrefix + || remoteFileName.Split('/', '\\').Any(segment => segment is "." or "..")) { - throw new InvalidOperationException("The remote file name must not contain '.' or '..' path segments."); + throw new InvalidOperationException( + "The remote file name must be relative and must not contain '.' or '..' path segments."); } } diff --git a/dotnet/src/Plugins/Plugins.UnitTests/Core/SessionsPythonPluginTests.cs b/dotnet/src/Plugins/Plugins.UnitTests/Core/SessionsPythonPluginTests.cs index 20080f6792e3..ce262555f183 100644 --- a/dotnet/src/Plugins/Plugins.UnitTests/Core/SessionsPythonPluginTests.cs +++ b/dotnet/src/Plugins/Plugins.UnitTests/Core/SessionsPythonPluginTests.cs @@ -288,7 +288,15 @@ public async Task ItShouldUploadFileAsync() [InlineData("nested/../test-file.txt")] [InlineData("nested/..")] [InlineData("/mnt/data/../../test-file.txt")] - public async Task ItShouldRejectUploadRemoteFileNamesWithDotSegmentsAsync(string remoteFileName) + [InlineData(@"nested\.\test-file.txt")] + [InlineData(@"nested\..\test-file.txt")] + [InlineData(@"\test-file.txt")] + [InlineData(@"\\server\share\test-file.txt")] + [InlineData(@"C:\test-file.txt")] + [InlineData("C:test-file.txt")] + [InlineData("C:/test-file.txt")] + [InlineData("/test-file.txt")] + public async Task ItShouldRejectUnsafeUploadRemoteFileNamesAsync(string remoteFileName) { // Arrange var plugin = new SessionsPythonPlugin(this._settingsWithFileOperationsEnabled, this._httpClientFactory); @@ -304,9 +312,10 @@ public async Task ItShouldRejectUploadRemoteFileNamesWithDotSegmentsAsync(string [Theory] [InlineData("nested/dir/test-file.txt")] + [InlineData(@"nested\dir\test-file.txt")] [InlineData(".hidden/test-file.txt")] [InlineData("nested/a..b/test-file.txt")] - public async Task ItShouldAllowUploadRemoteFileNamesWithoutDotSegmentsAsync(string remoteFileName) + public async Task ItShouldAllowSafeUploadRemoteFileNamesAsync(string remoteFileName) { // Arrange var responseContent = await File.ReadAllTextAsync(UpdaloadFileTestDataFilePath);