11package io .modelcontextprotocol .server .transport ;
22
33import java .io .IOException ;
4+ import java .util .List ;
45import java .util .Map ;
56import java .util .concurrent .ConcurrentHashMap ;
67
@@ -110,6 +111,11 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
110111 */
111112 private volatile boolean isClosing = false ;
112113
114+ /**
115+ * DNS rebinding protection configuration.
116+ */
117+ private final DnsRebindingProtectionConfig dnsRebindingProtectionConfig ;
118+
113119 /**
114120 * Constructs a new WebFlux SSE server transport provider instance with the default
115121 * SSE endpoint.
@@ -134,7 +140,7 @@ public WebFluxSseServerTransportProvider(ObjectMapper objectMapper, String messa
134140 * @throws IllegalArgumentException if either parameter is null
135141 */
136142 public WebFluxSseServerTransportProvider (ObjectMapper objectMapper , String messageEndpoint , String sseEndpoint ) {
137- this (objectMapper , DEFAULT_BASE_URL , messageEndpoint , sseEndpoint );
143+ this (objectMapper , DEFAULT_BASE_URL , messageEndpoint , sseEndpoint , null );
138144 }
139145
140146 /**
@@ -149,6 +155,24 @@ public WebFluxSseServerTransportProvider(ObjectMapper objectMapper, String messa
149155 */
150156 public WebFluxSseServerTransportProvider (ObjectMapper objectMapper , String baseUrl , String messageEndpoint ,
151157 String sseEndpoint ) {
158+ this (objectMapper , baseUrl , messageEndpoint , sseEndpoint , null );
159+ }
160+
161+ /**
162+ * Constructs a new WebFlux SSE server transport provider instance with optional DNS
163+ * rebinding protection.
164+ * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
165+ * of MCP messages. Must not be null.
166+ * @param baseUrl webflux message base path
167+ * @param messageEndpoint The endpoint URI where clients should send their JSON-RPC
168+ * messages. This endpoint will be communicated to clients during SSE connection
169+ * setup. Must not be null.
170+ * @param sseEndpoint The endpoint URI where clients establish their SSE connections.
171+ * @param dnsRebindingProtectionConfig The DNS rebinding protection configuration (may be null).
172+ * @throws IllegalArgumentException if required parameters are null
173+ */
174+ public WebFluxSseServerTransportProvider (ObjectMapper objectMapper , String baseUrl , String messageEndpoint ,
175+ String sseEndpoint , DnsRebindingProtectionConfig dnsRebindingProtectionConfig ) {
152176 Assert .notNull (objectMapper , "ObjectMapper must not be null" );
153177 Assert .notNull (baseUrl , "Message base path must not be null" );
154178 Assert .notNull (messageEndpoint , "Message endpoint must not be null" );
@@ -158,6 +182,7 @@ public WebFluxSseServerTransportProvider(ObjectMapper objectMapper, String baseU
158182 this .baseUrl = baseUrl ;
159183 this .messageEndpoint = messageEndpoint ;
160184 this .sseEndpoint = sseEndpoint ;
185+ this .dnsRebindingProtectionConfig = dnsRebindingProtectionConfig ;
161186 this .routerFunction = RouterFunctions .route ()
162187 .GET (this .sseEndpoint , this ::handleSseConnection )
163188 .POST (this .messageEndpoint , this ::handleMessage )
@@ -256,6 +281,16 @@ private Mono<ServerResponse> handleSseConnection(ServerRequest request) {
256281 return ServerResponse .status (HttpStatus .SERVICE_UNAVAILABLE ).bodyValue ("Server is shutting down" );
257282 }
258283
284+ // Validate headers
285+ if (dnsRebindingProtectionConfig != null ) {
286+ String hostHeader = request .headers ().asHttpHeaders ().getFirst ("Host" );
287+ String originHeader = request .headers ().asHttpHeaders ().getFirst ("Origin" );
288+ if (!dnsRebindingProtectionConfig .validate (hostHeader , originHeader )) {
289+ logger .warn ("DNS rebinding protection validation failed - Host: '{}', Origin: '{}'" , hostHeader , originHeader );
290+ return ServerResponse .status (HttpStatus .FORBIDDEN ).bodyValue ("DNS rebinding protection validation failed" );
291+ }
292+ }
293+
259294 return ServerResponse .ok ()
260295 .contentType (MediaType .TEXT_EVENT_STREAM )
261296 .body (Flux .<ServerSentEvent <?>>create (sink -> {
@@ -300,6 +335,25 @@ private Mono<ServerResponse> handleMessage(ServerRequest request) {
300335 return ServerResponse .status (HttpStatus .SERVICE_UNAVAILABLE ).bodyValue ("Server is shutting down" );
301336 }
302337
338+ // Always validate Content-Type for POST requests
339+ String contentType = request .headers ().contentType ()
340+ .map (MediaType ::toString )
341+ .orElse (null );
342+ if (contentType == null || !contentType .toLowerCase ().startsWith ("application/json" )) {
343+ logger .warn ("Invalid Content-Type header: '{}'" , contentType );
344+ return ServerResponse .badRequest ().bodyValue (new McpError ("Content-Type must be application/json" ));
345+ }
346+
347+ // Validate headers for POST requests if DNS rebinding protection is configured
348+ if (dnsRebindingProtectionConfig != null ) {
349+ String hostHeader = request .headers ().asHttpHeaders ().getFirst ("Host" );
350+ String originHeader = request .headers ().asHttpHeaders ().getFirst ("Origin" );
351+ if (!dnsRebindingProtectionConfig .validate (hostHeader , originHeader )) {
352+ logger .warn ("DNS rebinding protection validation failed - Host: '{}', Origin: '{}'" , hostHeader , originHeader );
353+ return ServerResponse .status (HttpStatus .FORBIDDEN ).bodyValue ("DNS rebinding protection validation failed" );
354+ }
355+ }
356+
303357 if (request .queryParam ("sessionId" ).isEmpty ()) {
304358 return ServerResponse .badRequest ().bodyValue (new McpError ("Session ID missing in message endpoint" ));
305359 }
@@ -397,6 +451,8 @@ public static class Builder {
397451
398452 private String sseEndpoint = DEFAULT_SSE_ENDPOINT ;
399453
454+ private DnsRebindingProtectionConfig dnsRebindingProtectionConfig ;
455+
400456 /**
401457 * Sets the ObjectMapper to use for JSON serialization/deserialization of MCP
402458 * messages.
@@ -447,6 +503,23 @@ public Builder sseEndpoint(String sseEndpoint) {
447503 return this ;
448504 }
449505
506+
507+ /**
508+ * Sets the DNS rebinding protection configuration.
509+ * <p>
510+ * When set, this configuration will be used to create a header validator that
511+ * enforces DNS rebinding protection rules. This will override any previously set
512+ * header validator.
513+ * @param config The DNS rebinding protection configuration
514+ * @return this builder instance
515+ * @throws IllegalArgumentException if config is null
516+ */
517+ public Builder dnsRebindingProtectionConfig (DnsRebindingProtectionConfig config ) {
518+ Assert .notNull (config , "DNS rebinding protection config must not be null" );
519+ this .dnsRebindingProtectionConfig = config ;
520+ return this ;
521+ }
522+
450523 /**
451524 * Builds a new instance of {@link WebFluxSseServerTransportProvider} with the
452525 * configured settings.
@@ -457,7 +530,8 @@ public WebFluxSseServerTransportProvider build() {
457530 Assert .notNull (objectMapper , "ObjectMapper must be set" );
458531 Assert .notNull (messageEndpoint , "Message endpoint must be set" );
459532
460- return new WebFluxSseServerTransportProvider (objectMapper , baseUrl , messageEndpoint , sseEndpoint );
533+ return new WebFluxSseServerTransportProvider (objectMapper , baseUrl , messageEndpoint , sseEndpoint ,
534+ dnsRebindingProtectionConfig );
461535 }
462536
463537 }
0 commit comments