Skip to content

Commit 2c5d1ed

Browse files
jeffhandleyCopilothalter73
authored
Document and demonstrate allowed hosts and CORS policy guidance (#1554)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Stephen Halter <halter73@gmail.com>
1 parent 0781bea commit 2c5d1ed

14 files changed

Lines changed: 288 additions & 9 deletions

File tree

docs/concepts/getting-started.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,20 @@ public static class EchoTool
101101
}
102102
```
103103

104+
#### Host name validation
105+
106+
For local HTTP servers, keep the set of accepted host names limited to loopback values. This helps protect against DNS rebinding, where a browser reaches a local server through an attacker-controlled DNS name while sending that DNS name in the HTTP `Host` header. ASP.NET Core's Kestrel server doesn't validate `Host` headers by default, so configure `AllowedHosts` with known host names rather than `"*"`.
107+
108+
For production servers, configure the exact public host names for the deployment, and validate the host name at the proxy or load balancer when one is responsible for forwarding client requests. This also avoids reflecting untrusted host names through ASP.NET Core features such as absolute URL generation. See [Host filtering with ASP.NET Core Kestrel web server | Microsoft Learn](https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel/host-filtering) and [URL generation concepts | Microsoft Learn](https://learn.microsoft.com/aspnet/core/fundamentals/routing#url-generation-concepts).
109+
110+
#### Browser cross-origin access
111+
112+
**Only** enable CORS if you intentionally want browser-based cross-origin access to this server.
113+
114+
CORS is not a substitute for host name validation. When browser-based cross-origin access is required, limit which browser origins can call the MCP endpoint by using the most restrictive ASP.NET Core CORS policy possible. See [Enable Cross-Origin Requests (CORS) in ASP.NET Core | Microsoft Learn](https://learn.microsoft.com/aspnet/core/security/cors).
115+
116+
For the full HTTP security examples, including `AllowedHosts` and restrictive CORS on `MapMcp`, see [Streamable HTTP transport](transports/transports.md#browser-cross-origin-access).
117+
104118
### Building an MCP client
105119

106120
Create a new console app, add the package, and replace `Program.cs` with the code below. This client connects to the MCP "everything" reference server, lists its tools, and calls one:

docs/concepts/httpcontext/samples/appsettings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
"Microsoft.AspNetCore": "Warning"
66
}
77
},
8-
"AllowedHosts": "*"
8+
"AllowedHosts": "localhost;127.0.0.1;[::1]"
99
}

docs/concepts/logging/samples/server/appsettings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
"Microsoft.AspNetCore": "Warning"
66
}
77
},
8-
"AllowedHosts": "*"
8+
"AllowedHosts": "localhost;127.0.0.1;[::1]"
99
}

docs/concepts/progress/samples/server/appsettings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
"Microsoft.AspNetCore": "Warning"
66
}
77
},
8-
"AllowedHosts": "*"
8+
"AllowedHosts": "localhost;127.0.0.1;[::1]"
99
}

docs/concepts/transports/transports.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,64 @@ app.Run();
133133

134134
By default, the HTTP transport uses **stateful sessions** — the server assigns an `Mcp-Session-Id` to each client and tracks session state in memory. For most servers, **stateless mode is recommended** instead. It simplifies deployment, enables horizontal scaling without session affinity, and avoids issues with clients that don't send the `Mcp-Session-Id` header. We recommend setting `Stateless` explicitly (rather than relying on the current default) for [forward compatibility](xref:stateless#forward-and-backward-compatibility). See [Sessions](xref:stateless) for a detailed guide on when to use stateless vs. stateful mode, configure session options, and understand [cancellation and disposal](xref:stateless#cancellation-and-disposal) behavior during shutdown.
135135

136+
#### Host name validation
137+
138+
For local HTTP servers, keep the set of accepted host names limited to loopback values. This helps protect against DNS rebinding, where a browser reaches a local server through an attacker-controlled DNS name while sending that DNS name in the HTTP `Host` header. ASP.NET Core's Kestrel server doesn't validate `Host` headers by default, so configure `AllowedHosts` with known host names rather than `"*"`. This also avoids reflecting untrusted host names through ASP.NET Core features such as absolute URL generation. See [Host filtering with ASP.NET Core Kestrel web server | Microsoft Learn](https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel/host-filtering) and [URL generation concepts | Microsoft Learn](https://learn.microsoft.com/aspnet/core/fundamentals/routing#url-generation-concepts).
139+
140+
```json
141+
// appsettings.Development.json
142+
{
143+
"AllowedHosts": "localhost;127.0.0.1;[::1]"
144+
}
145+
```
146+
147+
For production servers, configure `AllowedHosts` to the exact public host names for the deployment. If Kestrel is behind a reverse proxy or load balancer, validate the host name at the layer that receives or forwards the client `Host` header. ASP.NET Core's Host Filtering Middleware is appropriate when Kestrel is public-facing or the `Host` header is directly forwarded; Forwarded Headers Middleware has its own `AllowedHosts` option for cases where the proxy doesn't preserve the original `Host` header. See [Host filtering with ASP.NET Core Kestrel web server | Microsoft Learn](https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel/host-filtering) and [Configure ASP.NET Core to work with proxy servers and load balancers | Microsoft Learn](https://learn.microsoft.com/aspnet/core/host-and-deploy/proxy-load-balancer).
148+
149+
If you intentionally expose the server through another host name, such as a tunnel, container host, reverse proxy, or deployed domain, add that exact host name to `AllowedHosts` instead of using `"*"`.
150+
151+
#### Browser cross-origin access
152+
153+
**Only** enable CORS if you intentionally want browser-based cross-origin access to this server.
154+
155+
CORS is not a substitute for host name validation. When browser-based cross-origin access is required, limit which browser origins can call the MCP endpoint by using the most restrictive ASP.NET Core CORS policy possible. See [Enable Cross-Origin Requests (CORS) in ASP.NET Core | Microsoft Learn](https://learn.microsoft.com/aspnet/core/security/cors).
156+
157+
For a **stateless** browser client, a narrowly scoped CORS policy usually only needs the headers the browser would otherwise preflight: `Content-Type` for JSON, `Authorization` when the endpoint is protected, and `MCP-Protocol-Version`. If you enable sessions or resumability, also allow `Mcp-Session-Id` and `Last-Event-ID`, and expose `Mcp-Session-Id` on responses so browser code can read it. `Accept` normally doesn't need to be listed because browsers can already send it without extra CORS configuration.
158+
159+
160+
_In this sample below, the MCP server will allow browser calls from `localhost:5173` where a web application is making the request. In production, this allowed origin list would be configured to the trusted web application domains._
161+
162+
```json
163+
// appsettings.Development.json
164+
{
165+
"Mcp": {
166+
"AllowedOrigins": [
167+
"http://localhost:5173"
168+
]
169+
}
170+
}
171+
```
172+
173+
```csharp
174+
var allowedOrigins = builder.Configuration.GetSection("Mcp:AllowedOrigins").Get<string[]>() ?? ["http://localhost:5173"];
175+
176+
builder.Services.AddCors(options =>
177+
{
178+
options.AddPolicy("McpBrowserClient", policy =>
179+
{
180+
policy.WithOrigins(allowedOrigins)
181+
// Add GET for standalone/resumable SSE streams and DELETE for stateful session termination.
182+
.WithMethods("POST", "GET", "DELETE")
183+
.WithHeaders("Content-Type", "Authorization", "MCP-Protocol-Version", "Mcp-Session-Id")
184+
.WithExposedHeaders("Mcp-Session-Id");
185+
});
186+
});
187+
188+
var app = builder.Build();
189+
190+
app.UseCors();
191+
app.MapMcp("/mcp").RequireCors("McpBrowserClient");
192+
```
193+
136194
#### How messages flow
137195

138196
In Streamable HTTP, client requests arrive as HTTP POST requests. The server holds each POST response body open as an SSE stream and writes the JSON-RPC response — plus any intermediate messages like progress notifications or server-to-client requests — back through it. This provides natural HTTP-level backpressure: each POST holds its connection until the handler completes.

samples/AspNetCoreMcpPerSessionTools/appsettings.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@
66
"AspNetCoreMcpPerSessionTools": "Debug"
77
}
88
},
9-
"AllowedHosts": "*"
10-
}
9+
"AllowedHosts": "localhost;127.0.0.1;[::1]"
10+
}

samples/AspNetCoreMcpServer/Program.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,23 @@
66
using System.Net.Http.Headers;
77

88
var builder = WebApplication.CreateBuilder(args);
9+
var allowedOrigins = builder.Configuration.GetSection("Mcp:AllowedOrigins").Get<string[]>() ?? ["http://localhost:5173"];
10+
11+
// Only enable CORS if you intentionally want browser-based cross-origin access to this server.
12+
// Keep the allowlist narrowly scoped to known origins. Broad CORS settings weaken security.
13+
builder.Services.AddCors(options =>
14+
{
15+
options.AddPolicy("McpBrowserClient", policy =>
16+
{
17+
policy.WithOrigins(allowedOrigins)
18+
.WithMethods("GET", "POST", "DELETE")
19+
// Browsers can send Accept without extra CORS configuration. These are the MCP-specific
20+
// and non-safelisted headers the browser-based client needs for stateful Streamable HTTP.
21+
.WithHeaders("Content-Type", "MCP-Protocol-Version", "Mcp-Session-Id")
22+
.WithExposedHeaders("Mcp-Session-Id");
23+
});
24+
});
25+
926
// Note: This sample uses SampleLlmTool which calls server.AsSamplingChatClient() to send
1027
// a server-to-client sampling request. This requires stateful (session-based) mode. Set
1128
// Stateless = false explicitly for forward compatibility in case the default changes.
@@ -36,6 +53,7 @@
3653

3754
var app = builder.Build();
3855

39-
app.MapMcp();
56+
app.UseCors();
57+
app.MapMcp().RequireCors("McpBrowserClient");
4058

4159
app.Run();

samples/AspNetCoreMcpServer/appsettings.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,10 @@
55
"Microsoft.AspNetCore": "Warning"
66
}
77
},
8-
"AllowedHosts": "*"
8+
"AllowedHosts": "localhost;127.0.0.1;[::1]",
9+
"Mcp": {
10+
"AllowedOrigins": [
11+
"http://localhost:5173"
12+
]
13+
}
914
}

samples/EverythingServer/appsettings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
"Microsoft.AspNetCore": "Warning"
66
}
77
},
8-
"AllowedHosts": "*"
8+
"AllowedHosts": "localhost;127.0.0.1;[::1]"
99
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning"
6+
}
7+
},
8+
"AllowedHosts": "localhost;127.0.0.1;[::1]"
9+
}

0 commit comments

Comments
 (0)