Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci-build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:

# Keep version in sync with McpConformanceVersion in Directory.Packages.props
- name: 📦 Install conformance test runner
run: npm install @modelcontextprotocol/conformance@0.1.10
run: npm install @modelcontextprotocol/conformance@0.1.11
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should go back to testing against the latest version? I know it will be annoying when it causes random PRs to fail, but we'll definitely notice that way. And the InvalidOperationException we'd get from GetConformanceVersion() when they do fall out of sync should be pretty clear.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok by me.

Copy link
Copy Markdown
Contributor

@halter73 halter73 Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
run: npm install @modelcontextprotocol/conformance@0.1.11
run: npm install @modelcontextprotocol/conformance

@copilot Also update the message in the InvalidOperationException thrown by GetConformanceVersion() to tell the developer to start pinning the version in ci-build-test.yml and file an issue to update the conformance tests if they run into it.


- name: 🏗️ Build
run: make build CONFIGURATION=${{ matrix.configuration }}
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<MicrosoftExtensionsVersion>10.2.0</MicrosoftExtensionsVersion>
<!-- Pin the conformance tester Node package version for CI stability.
Keep in sync npm install step at ci-build-test.yml -->
<McpConformanceVersion>0.1.10</McpConformanceVersion>
<McpConformanceVersion>0.1.11</McpConformanceVersion>
</PropertyGroup>

<!-- Product dependencies shared -->
Expand Down
1 change: 0 additions & 1 deletion src/Common/Polyfills/System/IO/StreamExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using ModelContextProtocol;
using System.Buffers;
using System.Runtime.InteropServices;
using System.Text;

#if !NET
namespace System.IO;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;

namespace ModelContextProtocol.AspNetCore;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
using System.Net;
using System.Text.Json;

namespace ModelContextProtocol.AspNetCore;

/// <summary>
/// Middleware that provides DNS rebinding protection for MCP servers by validating
/// Host and Origin headers on requests to localhost servers.
/// </summary>
/// <remarks>
/// <para>
/// DNS rebinding attacks can allow malicious websites to bypass browser same-origin policy
/// and make requests to localhost services. This middleware helps protect against such attacks
/// by validating that Host and Origin headers match expected localhost values.
/// </para>
/// <para>
/// Use <see cref="McpApplicationBuilderExtensions.UseMcpDnsRebindingProtection"/> to enable this middleware.
/// </para>
/// </remarks>
/// <remarks>
/// Initializes a new instance of the <see cref="DnsRebindingProtectionMiddleware"/> class.
/// </remarks>
internal sealed partial class DnsRebindingProtectionMiddleware(
Comment thread
stephentoub marked this conversation as resolved.
Outdated
RequestDelegate next,
ILogger<DnsRebindingProtectionMiddleware> logger)
{
private readonly RequestDelegate _next = next;
private readonly ILogger<DnsRebindingProtectionMiddleware> _logger = logger;

/// <summary>
/// Processes the HTTP request and validates Host and Origin headers for localhost servers.
/// </summary>
public async Task InvokeAsync(HttpContext context)
{
// Only apply protection to localhost servers
var localEndpoint = context.Connection.LocalIpAddress;
bool isLocalhostServer = localEndpoint is null ||
IPAddress.IsLoopback(localEndpoint) ||
localEndpoint.Equals(IPAddress.IPv6Loopback);

if (isLocalhostServer)
{
// Validate Host header
var host = context.Request.Host.Host;
if (!IsLocalhost(host))
{
LogInvalidHostHeader(host);
await WriteJsonRpcErrorResponseAsync(context, $"Forbidden: Invalid Host header '{host}' for localhost server");
return;
}

// Validate Origin header if present
if (context.Request.Headers.TryGetValue(HeaderNames.Origin, out var originValues) &&
originValues.FirstOrDefault() is string origin &&
Uri.TryCreate(origin, UriKind.Absolute, out var originUri) &&
!IsLocalhost(originUri.Host))
{
LogInvalidOriginHeader(origin);
await WriteJsonRpcErrorResponseAsync(context, $"Forbidden: Invalid Origin header '{origin}' for localhost server");
return;
}
}

await _next(context).ConfigureAwait(false);
}

private static bool IsLocalhost(string host)
{
if (!string.IsNullOrWhiteSpace(host))
{
if (host.Equals("localhost", StringComparison.OrdinalIgnoreCase) ||
host.Equals("[::1]") ||
host.Equals("127.0.0.1"))
{
return true;
}

if (IPAddress.TryParse(host, out var ip))
{
return IPAddress.IsLoopback(ip);
}
}

return false;
}

private static Task WriteJsonRpcErrorResponseAsync(HttpContext context, string message)
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
context.Response.ContentType = "application/json";
return context.Response.WriteAsync($$"""
{
"jsonrpc": "2.0",
"error":
{
"code": -32000,
"message": "{{JsonEncodedText.Encode(message)}}"
},
"id": null
}
""");
}

[LoggerMessage(Level = LogLevel.Warning, Message = "Rejected request with invalid Host header '{Host}' for localhost server. This may indicate a DNS rebinding attack.")]
private partial void LogInvalidHostHeader(string? host);

[LoggerMessage(Level = LogLevel.Warning, Message = "Rejected request with invalid Origin header '{Origin}' for localhost server. This may indicate a DNS rebinding attack.")]
private partial void LogInvalidOriginHeader(string origin);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using ModelContextProtocol.AspNetCore;

namespace Microsoft.AspNetCore.Builder;

/// <summary>
/// Extension methods for adding MCP middleware to an <see cref="IApplicationBuilder"/>.
/// </summary>
public static class McpApplicationBuilderExtensions
{
/// <summary>
/// Adds DNS rebinding protection middleware for MCP servers running on localhost.
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/>.</param>
/// <returns>The <see cref="IApplicationBuilder"/> for chaining.</returns>
/// <remarks>
/// <para>
/// This method provides protection against DNS rebinding attacks by validating that both
/// Host and Origin headers (when present) resolve to localhost addresses.
/// </para>
/// <para>
/// DNS rebinding attacks can allow malicious websites to bypass browser same-origin policy and make requests
/// to localhost services. This protection is recommended for any MCP server that binds to localhost.
/// </para>
/// <para>
/// For more information, see the <see href="https://github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-w48q-cv73-mx4w">MCP SDK security advisory</see>.
/// </para>
/// </remarks>
/// <example>
/// <code>
/// var builder = WebApplication.CreateBuilder(args);
/// builder.Services.AddMcpServer().WithHttpTransport();
///
/// var app = builder.Build();
/// app.UseMcpDnsRebindingProtection(); // Add before MapMcp()
/// app.MapMcp();
/// app.Run();
/// </code>
/// </example>
public static IApplicationBuilder UseMcpDnsRebindingProtection(this IApplicationBuilder app)
{
ArgumentNullException.ThrowIfNull(app);

app.UseMiddleware<DnsRebindingProtectionMiddleware>();

return app;
}
}
19 changes: 19 additions & 0 deletions src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,25 @@ public sealed class ClientOAuthOptions
/// </remarks>
public string? ClientSecret { get; set; }

/// <summary>
/// Gets or sets the private key in PEM format for JWT client assertion (private_key_jwt).
/// </summary>
/// <remarks>
/// When provided along with <see cref="JwtSigningAlgorithm"/>, the client will use JWT client
/// assertion (private_key_jwt) for token endpoint authentication instead of client_secret.
/// This is typically used for machine-to-machine authentication with client_credentials grant.
/// </remarks>
public string? JwtPrivateKeyPem { get; set; }

/// <summary>
/// Gets or sets the signing algorithm for JWT client assertion.
/// </summary>
/// <remarks>
/// Common values include "RS256", "RS384", "RS512", "ES256", "ES384", "ES512".
/// This property is only used when <see cref="JwtPrivateKeyPem"/> is provided.
/// </remarks>
public string? JwtSigningAlgorithm { get; set; }

/// <summary>
/// Gets or sets the HTTPS URL pointing to this client's metadata document.
/// </summary>
Expand Down
Loading
Loading