From 1a427164ba15959869a7df52bc170e8e360a5d81 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 16 Sep 2026 09:15:58 -0400 Subject: [PATCH 1/2] tests(workflows): adds additional tests for metadata uri access --- .../ObjectModel/HttpRequestExecutorTest.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/HttpRequestExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/HttpRequestExecutorTest.cs index bba8b4d64d3..11b8cb0ccb6 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/HttpRequestExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/HttpRequestExecutorTest.cs @@ -20,6 +20,11 @@ public sealed class HttpRequestExecutorTest(ITestOutputHelper output) : Workflow { private const string TestUrl = "https://api.example.com/data"; + /// + /// Link-local metadata service address commonly used to test SSRF protections because it may expose cloud instance metadata. + /// + private const string LinkLocalMetadataUrl = "http://169.254.169.254/metadata/instance"; + private readonly Mock _agentProvider = new(MockBehavior.Loose); [Fact] @@ -75,6 +80,30 @@ public async Task HttpGetReturnsJsonObjectAsync() handler.VerifySent(info => info.Method == "GET" && info.Url == TestUrl); } + [Fact] + public async Task HttpRequestForwardsConversationInputControlledUrlToHandlerAsync() + { + // Arrange + this.State.InitializeSystem(); + this.State.Set(SystemScope.Names.LastMessageText, FormulaValue.New(LinkLocalMetadataUrl), VariableScopeNames.System); + this.State.Bind(); + + HttpRequestAction model = this.CreateModelWithVariableUrl( + displayName: nameof(HttpRequestForwardsConversationInputControlledUrlToHandlerAsync), + variablePath: "System.LastMessageText", + method: HttpMethodType.Get); + + MockHttpRequestHandler handler = new(HttpRequestResult("{}")); + HttpRequestExecutor action = new(model, handler.Object, this._agentProvider.Object, this.State); + + // Act + await this.ExecuteAsync(action); + + // Assert + VerifyModel(model, action); + handler.VerifySent(info => info.Method == "GET" && info.Url == LinkLocalMetadataUrl); + } + [Fact] public async Task HttpGetReturnsPlainStringAsync() { @@ -732,6 +761,20 @@ private HttpRequestAction CreateModel( return AssignParent(builder); } + private HttpRequestAction CreateModelWithVariableUrl(string displayName, string variablePath, HttpMethodType method) + { + HttpRequestAction.Builder builder = new() + { + Id = this.CreateActionId(), + DisplayName = this.FormatDisplayName(displayName), + Url = new StringExpression.Builder(StringExpression.Variable(PropertyPath.Create(variablePath))), + Method = new EnumExpression.Builder( + EnumExpression.Literal(HttpMethodTypeWrapper.Get(method))), + }; + + return AssignParent(builder); + } + private sealed class MockHttpRequestHandler : Mock { private HttpRequestInfo? _lastRequest; From ac37d3316f65a4e7d303de8259d8ec14da8fcbdb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 16 Sep 2026 13:28:21 +0000 Subject: [PATCH 2/2] Fix metadata URI canonicalization test coverage Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../DefaultHttpRequestHandlerTests.cs | 42 ++++++++++++++++++ .../ObjectModel/HttpRequestExecutorTest.cs | 43 ------------------- 2 files changed, 42 insertions(+), 43 deletions(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DefaultHttpRequestHandlerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DefaultHttpRequestHandlerTests.cs index 2d0cfbc8c1c..7c88a75994b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DefaultHttpRequestHandlerTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/DefaultHttpRequestHandlerTests.cs @@ -938,6 +938,48 @@ public async Task SendAsyncInvokesProviderWithCanonicalRequestUriAsync() Assert.Equal(requestUrls, providerUrls); } + [Fact] + public async Task SendAsyncInvokesProviderWithCanonicalMetadataRequestUriAsync() + { + // Arrange + CancellationToken cancellationToken = TestContext.Current.CancellationToken; + List providerUrls = []; + List requestUrls = []; + using HttpResponseMessage okResponse = new(HttpStatusCode.OK) + { + Content = new StringContent("ok", Encoding.UTF8, "text/plain"), + }; +#pragma warning disable CA2025 + TestHttpMessageHandler messageHandler = new((req, _) => + { + requestUrls.Add(req.RequestUri!.ToString()); + return Task.FromResult(okResponse); + }); +#pragma warning restore CA2025 + using HttpClient providerClient = new(messageHandler); +#pragma warning disable CA2025 + await using DefaultHttpRequestHandler handler = new((info, _) => + { + providerUrls.Add(info.Url); + return Task.FromResult(providerClient); + }); +#pragma warning restore CA2025 + + HttpRequestInfo request = new() + { + Method = "GET", + Url = "http://169.254.169.254/metadata/../latest/meta-data", + }; + + // Act + HttpRequestResult result = await handler.SendAsync(request, cancellationToken); + + // Assert + Assert.Equal("ok", result.Body); + Assert.Equal(["http://169.254.169.254/latest/meta-data"], requestUrls); + Assert.Equal(requestUrls, providerUrls); + } + [Fact] public async Task SendAsyncSuppliedClientReturnsRedirectResponseAsync() { diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/HttpRequestExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/HttpRequestExecutorTest.cs index 11b8cb0ccb6..bba8b4d64d3 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/HttpRequestExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/HttpRequestExecutorTest.cs @@ -20,11 +20,6 @@ public sealed class HttpRequestExecutorTest(ITestOutputHelper output) : Workflow { private const string TestUrl = "https://api.example.com/data"; - /// - /// Link-local metadata service address commonly used to test SSRF protections because it may expose cloud instance metadata. - /// - private const string LinkLocalMetadataUrl = "http://169.254.169.254/metadata/instance"; - private readonly Mock _agentProvider = new(MockBehavior.Loose); [Fact] @@ -80,30 +75,6 @@ public async Task HttpGetReturnsJsonObjectAsync() handler.VerifySent(info => info.Method == "GET" && info.Url == TestUrl); } - [Fact] - public async Task HttpRequestForwardsConversationInputControlledUrlToHandlerAsync() - { - // Arrange - this.State.InitializeSystem(); - this.State.Set(SystemScope.Names.LastMessageText, FormulaValue.New(LinkLocalMetadataUrl), VariableScopeNames.System); - this.State.Bind(); - - HttpRequestAction model = this.CreateModelWithVariableUrl( - displayName: nameof(HttpRequestForwardsConversationInputControlledUrlToHandlerAsync), - variablePath: "System.LastMessageText", - method: HttpMethodType.Get); - - MockHttpRequestHandler handler = new(HttpRequestResult("{}")); - HttpRequestExecutor action = new(model, handler.Object, this._agentProvider.Object, this.State); - - // Act - await this.ExecuteAsync(action); - - // Assert - VerifyModel(model, action); - handler.VerifySent(info => info.Method == "GET" && info.Url == LinkLocalMetadataUrl); - } - [Fact] public async Task HttpGetReturnsPlainStringAsync() { @@ -761,20 +732,6 @@ private HttpRequestAction CreateModel( return AssignParent(builder); } - private HttpRequestAction CreateModelWithVariableUrl(string displayName, string variablePath, HttpMethodType method) - { - HttpRequestAction.Builder builder = new() - { - Id = this.CreateActionId(), - DisplayName = this.FormatDisplayName(displayName), - Url = new StringExpression.Builder(StringExpression.Variable(PropertyPath.Create(variablePath))), - Method = new EnumExpression.Builder( - EnumExpression.Literal(HttpMethodTypeWrapper.Get(method))), - }; - - return AssignParent(builder); - } - private sealed class MockHttpRequestHandler : Mock { private HttpRequestInfo? _lastRequest;