Skip to content

Commit bf30e21

Browse files
committed
DefaultSseMessageEndpointValidator allows same-origin message endpoints
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
1 parent 563b155 commit bf30e21

2 files changed

Lines changed: 25 additions & 21 deletions

File tree

mcp-core/src/main/java/io/modelcontextprotocol/client/transport/DefaultSseMessageEndpointValidator.java

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010

1111
/**
1212
* Default {@link SseMessageEndpointValidator} that validates the {@code message} endpoint
13-
* advertised by an SSE server. Message endpoints must be a relative URI, without path
14-
* traversal or authority.
13+
* advertised by an SSE server. Message endpoints must either have the same origin as the
14+
* SSE uri, or be a relative uri.
1515
*
1616
* @author Daniel Garnier-Moiroux
1717
*/
@@ -30,16 +30,19 @@ public void validate(URI sseUri, String messageEndpoint) throws InvalidSseMessag
3030
messageEndpoint);
3131
}
3232

33-
if (endpointUri.isAbsolute()) {
34-
// Exclude absolute URIs e.g. https://example.com/mcp
35-
throw new InvalidSseMessageEndpointException("messageEndpoint must be a relative path, not an absolute URI",
36-
messageEndpoint);
37-
}
33+
if (endpointUri.isAbsolute() || endpointUri.getRawAuthority() != null) {
34+
String scheme = endpointUri.getScheme();
35+
String host = endpointUri.getHost();
36+
int port = endpointUri.getPort();
3837

39-
if (endpointUri.getRawAuthority() != null) {
40-
// Exclude network paths e.g. //example.com/mcp
41-
throw new InvalidSseMessageEndpointException(
42-
"messageEndpoint must be a relative path and must not contain an authority", messageEndpoint);
38+
boolean sameScheme = scheme != null && scheme.equalsIgnoreCase(sseUri.getScheme());
39+
boolean sameHost = host != null && host.equalsIgnoreCase(sseUri.getHost());
40+
boolean samePort = port == sseUri.getPort();
41+
42+
if (!sameScheme || !sameHost || !samePort) {
43+
throw new InvalidSseMessageEndpointException(
44+
"messageEndpoint must be a relative path or a same-origin URI", messageEndpoint);
45+
}
4346
}
4447

4548
// Exclude path-traversal

mcp-core/src/test/java/io/modelcontextprotocol/client/transport/DefaultSseMessageEndpointValidatorTests.java

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class DefaultSseMessageEndpointValidatorTests {
2525
private final DefaultSseMessageEndpointValidator validator = new DefaultSseMessageEndpointValidator();
2626

2727
@ParameterizedTest
28-
@ValueSource(strings = { "/messages", "messages?session=abc", "/" })
28+
@ValueSource(strings = { "/messages", "messages?session=abc", "/", "https://mcp.example.com/messages" })
2929
void valid(String endpoint) {
3030
assertThatCode(() -> validator.validate(SSE_URI, endpoint)).doesNotThrowAnyException();
3131
}
@@ -41,32 +41,33 @@ void invalidEmpty(String endpoint) {
4141
@ParameterizedTest
4242
@ValueSource(strings = { "/foo/../bar", "/foo/./bar", "../bar", "./bar", "/foo/%2E%2E/bar", "/foo/%2e/bar" })
4343
void invalidPathTraversal(String endpoint) {
44-
assertThatThrownBy(() -> validator.validate(SSE_URI, endpoint)).hasMessageContaining("path-traversal")
44+
assertThatThrownBy(() -> validator.validate(SSE_URI, endpoint))
45+
.hasMessageContaining("must not contain path-traversal segments")
4546
.asInstanceOf(type(InvalidSseMessageEndpointException.class))
4647
.extracting(InvalidSseMessageEndpointException::getMessageEndpoint)
4748
.isEqualTo(endpoint);
4849
}
4950

5051
@ParameterizedTest
51-
@ValueSource(strings = { "https://mcp.example.com/messages", "https://127.0.0.1/messages",
52-
"https://mcp.example.com:8443/messages", "http://localhost:1234/messages", "file:///etc/passwd",
53-
"gopher://mcp.example.com/_test" })
52+
@ValueSource(strings = { "https://127.0.0.1/messages", "https://mcp.example.com:8443/messages",
53+
"http://localhost:1234/messages", "file:///etc/passwd", "gopher://mcp.example.com/_test" })
5454
void invalidAbsoluteUris(String endpoint) {
55-
// Even an absolute URI on the same origin must be rejected: the contract
56-
// is that the messageEndpoint is a path-only relative reference.
57-
assertThatThrownBy(() -> validator.validate(SSE_URI, endpoint)).hasMessageContaining("must be a relative path")
55+
// Absolute URIs must be same-origin.
56+
assertThatThrownBy(() -> validator.validate(SSE_URI, endpoint))
57+
.hasMessageContaining("must be a relative path or a same-origin URI")
5858
.asInstanceOf(type(InvalidSseMessageEndpointException.class))
5959
.extracting(InvalidSseMessageEndpointException::getMessageEndpoint)
6060
.isEqualTo(endpoint);
6161

6262
}
6363

6464
@ParameterizedTest
65-
@ValueSource(strings = { "//example/messages", "//user:secret@example/messages" })
65+
@ValueSource(strings = { "//example/messages", "//user:secret@example/messages", "//mcp.example.com/messages" })
6666
void invalidNetworkReference(String endpoint) {
6767
// `//host/...` introduces an authority and is therefore not a pure path.
68+
// It is missing a scheme, so it fails same-origin check.
6869
assertThatThrownBy(() -> validator.validate(SSE_URI, endpoint))
69-
.hasMessageContaining("must not contain an authority")
70+
.hasMessageContaining("must be a relative path or a same-origin URI")
7071
.asInstanceOf(type(InvalidSseMessageEndpointException.class))
7172
.extracting(InvalidSseMessageEndpointException::getMessageEndpoint)
7273
.isEqualTo(endpoint);

0 commit comments

Comments
 (0)