DNS rebinding protection: check host header

Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
This commit is contained in:
Daniel Garnier-Moiroux 2026-02-05 16:12:04 +01:00 • committed by Daniel Garnier-Moiroux
commit f6c3fde7f4
2 changed files with 551 additions and 119 deletions

View file

@ -58,6 +58,8 @@ public class WebFluxServerTransportSecurityIntegrationTests {
private static final String DISALLOWED_ORIGIN = "https://malicious.example.com";
private static final String DISALLOWED_HOST = "malicious.example.com:8080";
@Parameter
private static Transport transport;
@ -79,7 +81,7 @@ public class WebFluxServerTransportSecurityIntegrationTests {
private McpSyncClient mcpClient;
private final TestOriginHeaderExchangeFilterFunction exchangeFilterFunction = new TestOriginHeaderExchangeFilterFunction();
private final TestHeaderExchangeFilterFunction exchangeFilterFunction = new TestHeaderExchangeFilterFunction();
@BeforeEach
void setUp() {
@ -125,6 +127,29 @@ public class WebFluxServerTransportSecurityIntegrationTests {
assertThatThrownBy(() -> mcpClient.listTools());
}
@Test
void hostAllowed() {
// Host header is set by default by WebClient to the request URI host
var result = mcpClient.initialize();
var tools = mcpClient.listTools();
assertThat(result.protocolVersion()).isNotEmpty();
assertThat(tools.tools()).isEmpty();
}
@Test
void connectHostNotAllowed() {
exchangeFilterFunction.setHostHeader(DISALLOWED_HOST);
assertThatThrownBy(() -> mcpClient.initialize());
}
@Test
void messageHostNotAllowed() {
mcpClient.initialize();
exchangeFilterFunction.setHostHeader(DISALLOWED_HOST);
assertThatThrownBy(() -> mcpClient.listTools());
}
// ----------------------------------------------------
// Server management
// ----------------------------------------------------
@ -165,7 +190,7 @@ public class WebFluxServerTransportSecurityIntegrationTests {
*/
interface Transport {
McpSyncClient createMcpClient(String baseUrl, TestOriginHeaderExchangeFilterFunction customizer);
McpSyncClient createMcpClient(String baseUrl, TestHeaderExchangeFilterFunction customizer);
RouterFunction<?> routerFunction();
@ -181,8 +206,10 @@ public class WebFluxServerTransportSecurityIntegrationTests {
public Sse() {
transportProvider = WebFluxSseServerTransportProvider.builder()
.messageEndpoint("/mcp/message")
.securityValidator(
DefaultServerTransportSecurityValidator.builder().allowedOrigin("http://localhost:*").build())
.securityValidator(DefaultServerTransportSecurityValidator.builder()
.allowedOrigin("http://localhost:*")
.allowedHost("localhost:*")
.build())
.build();
McpServer.sync(transportProvider)
.serverInfo("test-server", "1.0.0")
@ -191,8 +218,7 @@ public class WebFluxServerTransportSecurityIntegrationTests {
}
@Override
public McpSyncClient createMcpClient(String baseUrl,
TestOriginHeaderExchangeFilterFunction exchangeFilterFunction) {
public McpSyncClient createMcpClient(String baseUrl, TestHeaderExchangeFilterFunction exchangeFilterFunction) {
var transport = WebFluxSseClientTransport
.builder(WebClient.builder().baseUrl(baseUrl).filter(exchangeFilterFunction))
.jsonMapper(McpJsonDefaults.getDefaultMcpJsonMapper())
@ -213,8 +239,10 @@ public class WebFluxServerTransportSecurityIntegrationTests {
public StreamableHttp() {
transportProvider = WebFluxStreamableServerTransportProvider.builder()
.securityValidator(
DefaultServerTransportSecurityValidator.builder().allowedOrigin("http://localhost:*").build())
.securityValidator(DefaultServerTransportSecurityValidator.builder()
.allowedOrigin("http://localhost:*")
.allowedHost("localhost:*")
.build())
.build();
McpServer.sync(transportProvider)
.serverInfo("test-server", "1.0.0")
@ -223,8 +251,7 @@ public class WebFluxServerTransportSecurityIntegrationTests {
}
@Override
public McpSyncClient createMcpClient(String baseUrl,
TestOriginHeaderExchangeFilterFunction exchangeFilterFunction) {
public McpSyncClient createMcpClient(String baseUrl, TestHeaderExchangeFilterFunction exchangeFilterFunction) {
var transport = WebClientStreamableHttpTransport
.builder(WebClient.builder().baseUrl(baseUrl).filter(exchangeFilterFunction))
.jsonMapper(McpJsonDefaults.getDefaultMcpJsonMapper())
@ -246,8 +273,10 @@ public class WebFluxServerTransportSecurityIntegrationTests {
public Stateless() {
transportProvider = WebFluxStatelessServerTransport.builder()
.securityValidator(
DefaultServerTransportSecurityValidator.builder().allowedOrigin("http://localhost:*").build())
.securityValidator(DefaultServerTransportSecurityValidator.builder()
.allowedOrigin("http://localhost:*")
.allowedHost("localhost:*")
.build())
.build();
McpServer.sync(transportProvider)
.serverInfo("test-server", "1.0.0")
@ -256,8 +285,7 @@ public class WebFluxServerTransportSecurityIntegrationTests {
}
@Override
public McpSyncClient createMcpClient(String baseUrl,
TestOriginHeaderExchangeFilterFunction exchangeFilterFunction) {
public McpSyncClient createMcpClient(String baseUrl, TestHeaderExchangeFilterFunction exchangeFilterFunction) {
var transport = WebClientStreamableHttpTransport
.builder(WebClient.builder().baseUrl(baseUrl).filter(exchangeFilterFunction))
.jsonMapper(McpJsonDefaults.getDefaultMcpJsonMapper())
@ -273,18 +301,30 @@ public class WebFluxServerTransportSecurityIntegrationTests {
}
static class TestOriginHeaderExchangeFilterFunction implements ExchangeFilterFunction {
static class TestHeaderExchangeFilterFunction implements ExchangeFilterFunction {
private String origin = null;
private String host = null;
public void setOriginHeader(String origin) {
this.origin = origin;
}
public void setHostHeader(String host) {
this.host = host;
}
@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
var updatedRequest = ClientRequest.from(request).header("origin", this.origin).build();
return next.exchange(updatedRequest);
var builder = ClientRequest.from(request);
if (this.origin != null) {
builder.header("Origin", this.origin);
}
if (this.host != null) {
builder.header("Host", this.host);
}
return next.exchange(builder.build());
}
}

View file

@ -63,6 +63,8 @@ public class ServerTransportSecurityIntegrationTests {
private static final String DISALLOWED_ORIGIN = "https://malicious.example.com";
private static final String DISALLOWED_HOST = "malicious.example.com:8080";
@Parameter
private static Class<?> configClass;
@ -90,6 +92,7 @@ public class ServerTransportSecurityIntegrationTests {
void setUp() {
mcpClient = tomcatServer.appContext().getBean(McpSyncClient.class);
requestCustomizer = tomcatServer.appContext().getBean(TestRequestCustomizer.class);
requestCustomizer.reset();
}
@AfterEach
@ -131,6 +134,29 @@ public class ServerTransportSecurityIntegrationTests {
assertThatThrownBy(() -> mcpClient.listTools());
}
@Test
void hostAllowed() {
// Host header is set by default by HttpClient to the request URI host
var result = mcpClient.initialize();
var tools = mcpClient.listTools();
assertThat(result.protocolVersion()).isNotEmpty();
assertThat(tools.tools()).isEmpty();
}
@Test
void connectHostNotAllowed() {
requestCustomizer.setHostHeader(DISALLOWED_HOST);
assertThatThrownBy(() -> mcpClient.initialize());
}
@Test
void messageHostNotAllowed() {
mcpClient.initialize();
requestCustomizer.setHostHeader(DISALLOWED_HOST);
assertThatThrownBy(() -> mcpClient.listTools());
}
// ----------------------------------------------------
// Tomcat management
// ----------------------------------------------------
@ -195,7 +221,10 @@ public class ServerTransportSecurityIntegrationTests {
@Bean
DefaultServerTransportSecurityValidator validator() {
return DefaultServerTransportSecurityValidator.builder().allowedOrigin("http://localhost:*").build();
return DefaultServerTransportSecurityValidator.builder()
.allowedOrigin("http://localhost:*")
.allowedHost("localhost:*")
.build();
}
}
@ -318,18 +347,32 @@ public class ServerTransportSecurityIntegrationTests {
private String originHeader = null;
private String hostHeader = null;
@Override
public void customize(HttpRequest.Builder builder, String method, URI endpoint, String body,
McpTransportContext context) {
if (originHeader != null) {
builder.header("Origin", originHeader);
}
if (hostHeader != null) {
builder.header("Host", hostHeader);
}
}
public void setOriginHeader(String originHeader) {
this.originHeader = originHeader;
}
public void setHostHeader(String hostHeader) {
this.hostHeader = hostHeader;
}
public void reset() {
this.originHeader = null;
this.hostHeader = null;
}
}
}