From fe15e1b603e4e0afff2af0c787c82d67e30db0c8 Mon Sep 17 00:00:00 2001 From: Anzal Husain Abidi Date: Fri, 16 Oct 2026 23:10:53 +0530 Subject: [PATCH 1/2] Require digits-only port in wildcard-port Origin/Host matching The wildcard-port patterns (e.g. "localhost:*") used a bare prefix match, so values like "localhost:evil.com" or "http://localhost:3000.evil.com" were accepted. Require a non-empty ASCII-digits-only port after the prefix. --- ...faultServerTransportSecurityValidator.java | 26 ++++++++++++++-- ...ServerTransportSecurityValidatorTests.java | 30 +++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/DefaultServerTransportSecurityValidator.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/DefaultServerTransportSecurityValidator.java index 725a67aee..18540ad0f 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/DefaultServerTransportSecurityValidator.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/DefaultServerTransportSecurityValidator.java @@ -89,7 +89,7 @@ protected void validateOrigin(String origin) throws ServerTransportSecurityExcep else if (allowed.endsWith(":*")) { // Wildcard port pattern: "http://example.com:*" String baseOrigin = allowed.substring(0, allowed.length() - 2); - if (origin.equals(baseOrigin) || origin.startsWith(baseOrigin + ":")) { + if (origin.equals(baseOrigin) || matchesWithPort(origin, baseOrigin)) { return; } } @@ -121,7 +121,7 @@ private void validateHost(String host) throws ServerTransportSecurityException { else if (allowed.endsWith(":*")) { // Wildcard port pattern: "example.com:*" String baseHost = allowed.substring(0, allowed.length() - 2); - if (host.equals(baseHost) || host.startsWith(baseHost + ":")) { + if (host.equals(baseHost) || matchesWithPort(host, baseHost)) { return; } } @@ -130,6 +130,28 @@ else if (allowed.endsWith(":*")) { throw new ServerTransportSecurityException(421, "Invalid Host header"); } + /** + * Checks that {@code value} is {@code base} followed by {@code ":"} and a non-empty, + * ASCII-digits-only port. A bare prefix match would also accept values such as + * {@code localhost:evil.com} or {@code localhost:@evil.com}. + */ + private static boolean matchesWithPort(String value, String base) { + if (!value.startsWith(base + ":")) { + return false; + } + int portStart = base.length() + 1; + if (portStart == value.length()) { + return false; + } + for (int i = portStart; i < value.length(); i++) { + char c = value.charAt(i); + if (c < '0' || c > '9') { + return false; + } + } + return true; + } + /** * Creates a new builder for constructing a DefaultServerTransportSecurityValidator. * @return A new builder instance diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/transport/DefaultServerTransportSecurityValidatorTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/transport/DefaultServerTransportSecurityValidatorTests.java index 659dfeb2d..65b47a6c8 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/server/transport/DefaultServerTransportSecurityValidatorTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/transport/DefaultServerTransportSecurityValidatorTests.java @@ -10,6 +10,8 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -118,6 +120,16 @@ void differentPortWithWildcard() { assertThatCode(() -> wildcardValidator.validate(accessor)).doesNotThrowAnyException(); } + @ParameterizedTest + @ValueSource(strings = { "http://localhost:evil.com", "http://localhost:3000.evil.com", + "http://localhost:@evil.com", "http://localhost:", "http://localhost:80a", "http://localhost: 80", + "http://localhost:8080/path" }) + void nonNumericPortWithWildcard(String origin) { + var accessor = originAccessor(origin); + + assertThatThrownBy(() -> wildcardValidator.validate(accessor)).isEqualTo(INVALID_ORIGIN); + } + @Test void differentHostWithWildcard() { var accessor = originAccessor("http://example.com:3000"); @@ -273,6 +285,24 @@ void noPort() { assertThatCode(() -> wildcardHostValidator.validate(accessor)).doesNotThrowAnyException(); } + @ParameterizedTest + @ValueSource(strings = { "localhost:evil.com", "localhost:80.evil.com", "localhost:@evil.com", "localhost:", + "localhost:80a", "localhost: 80" }) + void nonNumericPort(String host) { + var accessor = hostAccessor(host); + + assertThatThrownBy(() -> wildcardHostValidator.validate(accessor)).isEqualTo(INVALID_HOST); + } + + @ParameterizedTest + @ValueSource(strings = { "127.0.0.1:evil.com", "127.0.0.1:80.evil.com", "127.0.0.1:@evil.com" }) + void nonNumericPortIpv4(String host) { + var ipValidator = DefaultServerTransportSecurityValidator.builder().allowedHost("127.0.0.1:*").build(); + + assertThatThrownBy(() -> ipValidator.validate(hostAccessor(host))).isEqualTo(INVALID_HOST); + assertThatCode(() -> ipValidator.validate(hostAccessor("127.0.0.1:8080"))).doesNotThrowAnyException(); + } + @Test void differentHost() { var accessor = hostAccessor("example.com:3000"); From 19424a3345cf289aaf975ed94b6db71239de8fe5 Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Tue, 20 Oct 2026 13:14:56 +0200 Subject: [PATCH 2/2] Restrict wildcard ports to the 1-65535 range Reject ports that are out of range (0, 65536) or longer than five digits, and use the same invalid-port cases for both Origin and Host. See #1192 Signed-off-by: Daniel Garnier-Moiroux --- ...faultServerTransportSecurityValidator.java | 20 ++++++------ ...ServerTransportSecurityValidatorTests.java | 32 +++++++++++++++---- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/DefaultServerTransportSecurityValidator.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/DefaultServerTransportSecurityValidator.java index 18540ad0f..7a39c0504 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/DefaultServerTransportSecurityValidator.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/DefaultServerTransportSecurityValidator.java @@ -131,25 +131,25 @@ else if (allowed.endsWith(":*")) { } /** - * Checks that {@code value} is {@code base} followed by {@code ":"} and a non-empty, - * ASCII-digits-only port. A bare prefix match would also accept values such as - * {@code localhost:evil.com} or {@code localhost:@evil.com}. + * Checks that {@code actual} is {@code allowed} followed by {@code ":"} and a valid + * port: 1 to 5 ASCII digits, in the range 1-65535. */ - private static boolean matchesWithPort(String value, String base) { - if (!value.startsWith(base + ":")) { + private static boolean matchesWithPort(String actual, String allowed) { + if (!actual.startsWith(allowed + ":")) { return false; } - int portStart = base.length() + 1; - if (portStart == value.length()) { + String port = actual.substring(allowed.length() + 1); + if (port.isEmpty() || port.length() > 5) { return false; } - for (int i = portStart; i < value.length(); i++) { - char c = value.charAt(i); + for (int i = 0; i < port.length(); i++) { + char c = port.charAt(i); if (c < '0' || c > '9') { return false; } } - return true; + int portNumber = Integer.parseInt(port); + return portNumber >= 1 && portNumber <= 65535; } /** diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/transport/DefaultServerTransportSecurityValidatorTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/transport/DefaultServerTransportSecurityValidatorTests.java index 65b47a6c8..7fab7b85e 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/server/transport/DefaultServerTransportSecurityValidatorTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/transport/DefaultServerTransportSecurityValidatorTests.java @@ -121,10 +121,19 @@ void differentPortWithWildcard() { } @ParameterizedTest - @ValueSource(strings = { "http://localhost:evil.com", "http://localhost:3000.evil.com", - "http://localhost:@evil.com", "http://localhost:", "http://localhost:80a", "http://localhost: 80", - "http://localhost:8080/path" }) - void nonNumericPortWithWildcard(String origin) { + @ValueSource(strings = { "http://localhost:1", "http://localhost:65535" }) + void boundaryPortsWithWildcard(String origin) { + var accessor = originAccessor(origin); + + assertThatCode(() -> wildcardValidator.validate(accessor)).doesNotThrowAnyException(); + } + + @ParameterizedTest + @ValueSource(strings = { "http://localhost:", "http://localhost:evil.com", "http://localhost:3000.evil.com", + "http://localhost:@evil.com", "http://localhost:80a", "http://localhost: 80", + "http://localhost:3000/path", "http://localhost:0", "http://localhost:65536", + "http://localhost:123456", "http://localhost:-1", "http://localhost:+80" }) + void invalidPortWithWildcard(String origin) { var accessor = originAccessor(origin); assertThatThrownBy(() -> wildcardValidator.validate(accessor)).isEqualTo(INVALID_ORIGIN); @@ -286,9 +295,18 @@ void noPort() { } @ParameterizedTest - @ValueSource(strings = { "localhost:evil.com", "localhost:80.evil.com", "localhost:@evil.com", "localhost:", - "localhost:80a", "localhost: 80" }) - void nonNumericPort(String host) { + @ValueSource(strings = { "localhost:1", "localhost:65535" }) + void boundaryPorts(String host) { + var accessor = hostAccessor(host); + + assertThatCode(() -> wildcardHostValidator.validate(accessor)).doesNotThrowAnyException(); + } + + @ParameterizedTest + @ValueSource(strings = { "localhost:", "localhost:evil.com", "localhost:3000.evil.com", + "localhost:@evil.com", "localhost:80a", "localhost: 80", "localhost:3000/path", "localhost:0", + "localhost:65536", "localhost:123456", "localhost:-1", "localhost:+80" }) + void invalidPort(String host) { var accessor = hostAccessor(host); assertThatThrownBy(() -> wildcardHostValidator.validate(accessor)).isEqualTo(INVALID_HOST);