DNS rebinding protection: check host header
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
This commit is contained in:
parent
b57b1269d0
commit
f6c3fde7f4
2 changed files with 551 additions and 119 deletions
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue