From e78be891a694c187d5b5a38c2a133b7250887a21 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 19 Oct 2026 11:04:16 -0400 Subject: [PATCH] fix(dotnet): validate MCP invocation headers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b7854637-38f5-42cd-8e94-bd46f8c06f01 --- .../DefaultMcpToolHandler.cs | 15 ++++++ ...Agents.AI.Workflows.Declarative.Mcp.csproj | 1 + .../DefaultMcpToolHandlerTests.cs | 53 +++++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/DefaultMcpToolHandler.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/DefaultMcpToolHandler.cs index 61aedec347f..9bd67e55eb4 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/DefaultMcpToolHandler.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/DefaultMcpToolHandler.cs @@ -104,6 +104,21 @@ public async Task InvokeToolAsync( ThrowIfListToolsArgumentsSpecified(arguments); } + if (headers is not null) + { + foreach (KeyValuePair header in headers) + { + HttpHeaderValidation.ValidateNoProhibitedCharacters( + header.Key, + nameof(headers), + "HTTP header name contains invalid characters."); + HttpHeaderValidation.ValidateNoProhibitedCharacters( + header.Value, + nameof(headers), + "HTTP header value contains invalid characters."); + } + } + if (this._httpClientProvider is not null) { TaskCompletionSource invocationCompleted = new(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/Microsoft.Agents.AI.Workflows.Declarative.Mcp.csproj b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/Microsoft.Agents.AI.Workflows.Declarative.Mcp.csproj index 08738266c09..0bb04cc608e 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/Microsoft.Agents.AI.Workflows.Declarative.Mcp.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Mcp/Microsoft.Agents.AI.Workflows.Declarative.Mcp.csproj @@ -7,6 +7,7 @@ true + true true true true diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.Mcp.UnitTests/DefaultMcpToolHandlerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.Mcp.UnitTests/DefaultMcpToolHandlerTests.cs index 610f7393eda..aa1fb0440aa 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.Mcp.UnitTests/DefaultMcpToolHandlerTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.Mcp.UnitTests/DefaultMcpToolHandlerTests.cs @@ -90,6 +90,59 @@ public async Task DisposeAsync_WhenCalledMultipleTimes_ShouldHandleGracefullyAsy #region HttpClientProvider Tests + [Theory] + [InlineData("\r", false, false)] + [InlineData("\n", false, false)] + [InlineData("\0", false, false)] + [InlineData("\r", true, false)] + [InlineData("\n", true, false)] + [InlineData("\0", true, false)] + [InlineData("\r", false, true)] + [InlineData("\n", false, true)] + [InlineData("\0", false, true)] + [InlineData("\r", true, true)] + [InlineData("\n", true, true)] + [InlineData("\0", true, true)] + public async Task InvokeToolAsync_WithInvalidHeader_ShouldRejectBeforeClientCreationAsync( + string invalidCharacter, + bool invalidName, + bool useProvider) + { + // Arrange + bool clientCreationAttempted = false; + Task ProviderAsync(string url, CancellationToken ct) + { + clientCreationAttempted = true; + throw new InvalidOperationException("Client creation should not be reached."); + } + + HttpMessageHandler CreateHandler() + { + clientCreationAttempted = true; + throw new InvalidOperationException("Client creation should not be reached."); + } + + await using DefaultMcpToolHandler handler = new(useProvider ? ProviderAsync : null, CreateHandler); + Dictionary headers = new() + { + [invalidName ? $"X-Test{invalidCharacter}" : "X-Test"] = invalidName ? "value" : $"value{invalidCharacter}" + }; + + // Act + async Task actAsync() => await handler.InvokeToolAsync( + serverUrl: "https://example.com/mcp", + serverLabel: null, + toolName: "testTool", + arguments: null, + headers: headers, + connectionName: null); + + // Assert + ArgumentException exception = await Assert.ThrowsAsync(actAsync); + Assert.Equal("headers", exception.ParamName); + Assert.False(clientCreationAttempted); + } + [Fact] public async Task InvokeToolAsync_WithHttpClientProvider_ShouldCallProviderAsync() {