From 382e3e66c6c00e91ec4ada5c2ee651e8d09e8652 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Thu, 7 May 2026 13:59:03 +0100 Subject: [PATCH 1/2] Fix OpenAPI server variable injection and path traversal vulnerabilities - Apply Uri.EscapeDataString to LLM-supplied server variable values before substitution in GetServerUrl (issue 115071) - Add ValidatePathSegments to reject dot-segments (. and ..) introduced by path parameter values, preventing path traversal (issue 115103) - Add 5 regression tests covering both fixes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Model/RestApiOperation.cs | 24 ++- .../OpenApi/RestApiOperationTests.cs | 150 ++++++++++++++++++ 2 files changed, 172 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperation.cs b/dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperation.cs index c2a5919514c4..84b0ec507c8a 100644 --- a/dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperation.cs +++ b/dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperation.cs @@ -308,6 +308,8 @@ private string BuildPath(string pathTemplate, IDictionary argum pathTemplate = pathTemplate.Replace($"{{{parameter.Name}}}", HttpUtility.UrlEncode(serializer.Invoke(parameter, node))); } + ValidatePathSegments(pathTemplate); + return pathTemplate; } @@ -364,14 +366,14 @@ private Uri GetServerUrl(Uri? serverUrlOverride, Uri? apiHostUrl, IDictionary + /// Validates that the path does not contain dot-segments (. or ..) that could enable path traversal. + /// ".." navigates up one path segment, enabling traversal to unintended endpoints. + /// "." refers to the current directory — harmless but unexpected, so rejected to prevent misuse. + /// + /// The path to validate. + private static void ValidatePathSegments(string path) + { + var segments = path.Split('/'); + for (int i = 0; i < segments.Length; i++) + { + if (segments[i] == "." || segments[i] == "..") + { + throw new KernelException($"Path '{path}' contains a dot-segment introduced by parameter values, which could lead to path traversal."); + } + } + } + private IDictionary _extensions = s_emptyDictionary; private readonly Freezable _freezable = new(); private string? _description; diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationTests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationTests.cs index c90869bc8912..c4375d9d940c 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationTests.cs @@ -1315,4 +1315,154 @@ public void ItShouldFreezeModifiableProperties() Assert.Throws(() => sut.Extensions.Add("x-fake", "fake_value")); } + + [Fact] + public void ItShouldEncodeServerVariableValuesFromArguments() + { + // Arrange — variable value contains path-manipulation characters + var version = new RestApiServerVariable("v1", null, ["v1", "v2/../admin"]); + var sut = new RestApiOperation( + id: "fake_id", + servers: [ + new RestApiServer("https://example.com/{version}", new Dictionary { { "version", version } }), + ], + path: "/items", + method: HttpMethod.Get, + description: "fake_description", + parameters: [], + responses: new Dictionary(), + securityRequirements: [] + ); + + var arguments = new Dictionary() { { "version", "v2/../admin" } }; + + // Act + var url = sut.BuildOperationUrl(arguments); + + // Assert — slashes and dots must be percent-encoded, not interpreted as path separators + Assert.Equal("https://example.com/v2%2F..%2Fadmin/items", url.OriginalString); + } + + [Fact] + public void ItShouldPreventServerVariableInjectionWithSpecialCharacters() + { + // Arrange — variable value contains path traversal and query string injection + var host = new RestApiServerVariable("api.example.com"); + var sut = new RestApiOperation( + id: "fake_id", + servers: [ + new RestApiServer("https://{host}/api", new Dictionary { { "host", host } }), + ], + path: "/data", + method: HttpMethod.Get, + description: "fake_description", + parameters: [], + responses: new Dictionary(), + securityRequirements: [] + ); + + var arguments = new Dictionary() { { "host", "evil.com/hijack?q=1#" } }; + + // Act & Assert — encoding the malicious value makes the URI unparsable, preventing injection + Assert.ThrowsAny(() => sut.BuildOperationUrl(arguments)); + } + + [Fact] + public void ItShouldRejectDotSegmentInPathParameter() + { + // Arrange — path parameter value is ".." (dot-segment traversal) + var parameters = new List { + new( + name: "id", + type: "string", + isRequired: true, + expand: false, + location: RestApiParameterLocation.Path, + style: RestApiParameterStyle.Simple) + }; + + var sut = new RestApiOperation( + id: "fake_id", + servers: [new RestApiServer("https://example.com/api")], + path: "/resources/{id}/details", + method: HttpMethod.Get, + description: "fake_description", + parameters: parameters, + responses: new Dictionary(), + securityRequirements: [] + ); + + var arguments = new Dictionary { { "id", ".." } }; + + // Act & Assert — dot-segments must be rejected + var ex = Assert.Throws(() => sut.BuildOperationUrl(arguments)); + Assert.Contains("dot-segment", ex.Message); + } + + [Fact] + public void ItShouldRejectSingleDotSegmentInPathParameter() + { + // Arrange — path parameter value is "." (single-dot segment) + var parameters = new List { + new( + name: "id", + type: "string", + isRequired: true, + expand: false, + location: RestApiParameterLocation.Path, + style: RestApiParameterStyle.Simple) + }; + + var sut = new RestApiOperation( + id: "fake_id", + servers: [new RestApiServer("https://example.com/api")], + path: "/resources/{id}/details", + method: HttpMethod.Get, + description: "fake_description", + parameters: parameters, + responses: new Dictionary(), + securityRequirements: [] + ); + + var arguments = new Dictionary { { "id", "." } }; + + // Act & Assert — single-dot segments must also be rejected + var ex = Assert.Throws(() => sut.BuildOperationUrl(arguments)); + Assert.Contains("dot-segment", ex.Message); + } + + [Fact] + public void ItShouldAllowDotsInNonSegmentPathParameterValues() + { + // Arrange — path parameter contains dots but is NOT a dot-segment (e.g., "file.txt") + var parameters = new List { + new( + name: "filename", + type: "string", + isRequired: true, + expand: false, + location: RestApiParameterLocation.Path, + style: RestApiParameterStyle.Simple) + }; + + var sut = new RestApiOperation( + id: "fake_id", + servers: [new RestApiServer("https://example.com/api")], + path: "/files/{filename}", + method: HttpMethod.Get, + description: "fake_description", + parameters: parameters, + responses: new Dictionary(), + securityRequirements: [] + ); + + var arguments = new Dictionary { { "filename", "report.v2.txt" } }; + + // Act + var url = sut.BuildOperationUrl(arguments); + + // Assert — dots within normal filenames should work fine + Assert.Equal("https://example.com/api/files/report.v2.txt", url.OriginalString); + } + } From 0daff71b8248bc74bdb6688c73a7f595ed508b98 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Thu, 7 May 2026 14:20:14 +0100 Subject: [PATCH 2/2] Address PR review feedback - Use accumulated serverUrlString for multi-variable substitution - Add ArgumentName branch encoding test - Fix error message in ValidatePathSegments - Improve test comments for clarity Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Model/RestApiOperation.cs | 8 ++--- .../OpenApi/RestApiOperationTests.cs | 32 +++++++++++++++++-- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperation.cs b/dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperation.cs index 84b0ec507c8a..5bfa5a8a1ff2 100644 --- a/dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperation.cs +++ b/dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperation.cs @@ -366,19 +366,19 @@ private Uri GetServerUrl(Uri? serverUrlOverride, Uri? apiHostUrl, IDictionary() { { "host", "evil.com/hijack?q=1#" } }; - // Act & Assert — encoding the malicious value makes the URI unparsable, preventing injection + // Act & Assert — encoding turns /, ?, # into percent-encoded sequences (%2F, %3F, %23), + // which prevents them from being interpreted as structural URI delimiters. + // The Uri constructor rejects the resulting hostname, which is the desired outcome. Assert.ThrowsAny(() => sut.BuildOperationUrl(arguments)); } @@ -1465,4 +1467,30 @@ public void ItShouldAllowDotsInNonSegmentPathParameterValues() Assert.Equal("https://example.com/api/files/report.v2.txt", url.OriginalString); } + [Fact] + public void ItShouldEncodeServerVariableValuesLookedUpByArgumentName() + { + // Arrange — variable uses ArgumentName and the argument contains path-manipulation characters + var version = new RestApiServerVariable("v1", null, ["v1", "v2/../admin"]) { ArgumentName = "alt_version" }; + var sut = new RestApiOperation( + id: "fake_id", + servers: [ + new RestApiServer("https://example.com/{version}", new Dictionary { { "version", version } }), + ], + path: "/items", + method: HttpMethod.Get, + description: "fake_description", + parameters: [], + responses: new Dictionary(), + securityRequirements: [] + ); + + var arguments = new Dictionary() { { "alt_version", "v2/../admin" } }; + + // Act + var url = sut.BuildOperationUrl(arguments); + + // Assert — reserved separators must be percent-encoded even when looked up via ArgumentName + Assert.Equal("https://example.com/v2%2F..%2Fadmin/items", url.OriginalString); + } }