Skip to content

Commit 8c0fdd1

Browse files
committed
Bound HTTP server reads
- Tests are not in the abstract base class because the spring transports are not vulnerable Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
1 parent 34ee267 commit 8c0fdd1

11 files changed

Lines changed: 659 additions & 37 deletions

File tree

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,10 @@ else if (statusCode == BAD_REQUEST) {
601601
return Flux.<McpSchema.JSONRPCMessage>error(new McpTransportException(
602602
"Bad Request. Status code:" + statusCode + ", response-event:" + responseEvent));
603603
}
604+
else if (statusCode >= 400 && statusCode < 500) {
605+
return Flux.<McpSchema.JSONRPCMessage>error(
606+
new McpTransportException("Invalid request. Status code: " + statusCode));
607+
}
604608

605609
return Flux.<McpSchema.JSONRPCMessage>error(
606610
new RuntimeException("Failed to send message: " + responseEvent));

mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletRequestUtils.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
package io.modelcontextprotocol.server.transport;
66

7+
import java.io.ByteArrayOutputStream;
8+
import java.io.IOException;
9+
import java.io.InputStream;
10+
import java.nio.charset.StandardCharsets;
711
import java.util.Collections;
812
import java.util.Enumeration;
913
import java.util.HashMap;
@@ -37,4 +41,32 @@ static Map<String, List<String>> extractHeaders(HttpServletRequest request) {
3741
return headers;
3842
}
3943

44+
/**
45+
* Reads the request body, decoded using the request's character encoding (or UTF-8 if
46+
* not specified), while bounding the number of bytes read.
47+
* @param request The HTTP servlet request
48+
* @param maxSize The maximum number of bytes to read from the request body
49+
* @return The decoded request body
50+
* @throws MaxSizeExceededException If the body exceeds {@code maxSize}
51+
* @throws IOException If an I/O error occurs while reading the request body
52+
*/
53+
static String readBody(HttpServletRequest request, int maxSize) throws MaxSizeExceededException, IOException {
54+
InputStream inputStream = request.getInputStream();
55+
ByteArrayOutputStream bodyBytes = new ByteArrayOutputStream();
56+
byte[] buf = new byte[8192];
57+
int totalBytes = 0;
58+
int readBytes;
59+
while ((readBytes = inputStream.read(buf, 0, buf.length)) != -1) {
60+
totalBytes += readBytes;
61+
if (totalBytes > maxSize) {
62+
throw new MaxSizeExceededException(
63+
"Request body exceeds the maximum allowed size of " + maxSize + " bytes");
64+
}
65+
bodyBytes.write(buf, 0, readBytes);
66+
}
67+
String charset = request.getCharacterEncoding() != null ? request.getCharacterEncoding()
68+
: StandardCharsets.UTF_8.name();
69+
return bodyBytes.toString(charset);
70+
}
71+
4072
}

mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
package io.modelcontextprotocol.server.transport;
66

7-
import java.io.BufferedReader;
87
import java.io.IOException;
98
import java.io.PrintWriter;
109
import java.time.Duration;
@@ -70,6 +69,11 @@
7069
@WebServlet(asyncSupported = true)
7170
public class HttpServletSseServerTransportProvider extends HttpServlet implements McpServerTransportProvider {
7271

72+
/**
73+
* Default maximum size of a single request body: 16 MiB (16 * 1024 * 1024 bytes).
74+
*/
75+
private static final int DEFAULT_REQUEST_MAX_SIZE = 16 * 1024 * 1024;
76+
7377
/**
7478
* Logger for this class
7579
*/
@@ -105,6 +109,11 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement
105109
*/
106110
private final McpJsonMapper jsonMapper;
107111

112+
/**
113+
* Maximum size, in bytes, of a single request body accepted by this transport.
114+
*/
115+
private final int requestMaxSize;
116+
108117
/**
109118
* Base URL for the server transport
110119
*/
@@ -160,24 +169,28 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement
160169
* keep-alive functionality
161170
* @param contextExtractor The extractor for transport context from the request.
162171
* @param securityValidator The security validator for validating HTTP requests.
172+
* @param requestMaxSize The maximum size, in bytes, of a single request body. Must be
173+
* positive.
163174
*/
164175
private HttpServletSseServerTransportProvider(McpJsonMapper jsonMapper, String baseUrl, String messageEndpoint,
165176
String sseEndpoint, Duration keepAliveInterval,
166177
McpTransportContextExtractor<HttpServletRequest> contextExtractor,
167-
ServerTransportSecurityValidator securityValidator) {
178+
ServerTransportSecurityValidator securityValidator, int requestMaxSize) {
168179

169180
Assert.notNull(jsonMapper, "JsonMapper must not be null");
170181
Assert.notNull(messageEndpoint, "messageEndpoint must not be null");
171182
Assert.notNull(sseEndpoint, "sseEndpoint must not be null");
172183
Assert.notNull(contextExtractor, "Context extractor must not be null");
173184
Assert.notNull(securityValidator, "Security validator must not be null");
185+
Assert.isTrue(requestMaxSize > 0, "requestMaxSize must be positive");
174186

175187
this.jsonMapper = jsonMapper;
176188
this.baseUrl = baseUrl;
177189
this.messageEndpoint = messageEndpoint;
178190
this.sseEndpoint = sseEndpoint;
179191
this.contextExtractor = contextExtractor;
180192
this.securityValidator = securityValidator;
193+
this.requestMaxSize = requestMaxSize;
181194

182195
if (keepAliveInterval != null) {
183196

@@ -321,6 +334,11 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
321334
return;
322335
}
323336

337+
if (request.getContentLengthLong() > this.requestMaxSize) {
338+
response.sendError(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE);
339+
return;
340+
}
341+
324342
String requestURI = request.getRequestURI();
325343
if (!requestURI.endsWith(messageEndpoint)) {
326344
response.sendError(HttpServletResponse.SC_NOT_FOUND);
@@ -363,22 +381,20 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
363381
}
364382

365383
try {
366-
BufferedReader reader = request.getReader();
367-
StringBuilder body = new StringBuilder();
368-
String line;
369-
while ((line = reader.readLine()) != null) {
370-
body.append(line);
371-
}
384+
String body = HttpServletRequestUtils.readBody(request, this.requestMaxSize);
372385

373386
final McpTransportContext transportContext = this.contextExtractor.extract(request);
374-
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body.toString());
387+
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body);
375388

376389
// Process the message through the session's handle method
377390
// Block for Servlet compatibility
378391
session.handle(message).contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext)).block();
379392

380393
response.setStatus(HttpServletResponse.SC_OK);
381394
}
395+
catch (MaxSizeExceededException e) {
396+
response.sendError(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE);
397+
}
382398
catch (Exception e) {
383399
logger.error("Error processing message: {}", e.getMessage());
384400
try {
@@ -574,6 +590,8 @@ public static class Builder {
574590

575591
private ServerTransportSecurityValidator securityValidator = ServerTransportSecurityValidator.NOOP;
576592

593+
private int requestMaxSize = DEFAULT_REQUEST_MAX_SIZE;
594+
577595
/**
578596
* Sets the JsonMapper implementation to use for serialization/deserialization. If
579597
* not specified, a JacksonJsonMapper will be created from the configured
@@ -660,6 +678,19 @@ public Builder securityValidator(ServerTransportSecurityValidator securityValida
660678
return this;
661679
}
662680

681+
/**
682+
* Sets the maximum size, in bytes, of a single request body accepted by this
683+
* transport. Requests whose body exceeds this size are rejected with a 413
684+
* (Payload Too Large) response. Defaults to 16 MiB if not set.
685+
* @param requestMaxSize The maximum request body size, in bytes. Must be
686+
* positive.
687+
* @return This builder instance
688+
*/
689+
public Builder maxRequestSize(int requestMaxSize) {
690+
this.requestMaxSize = requestMaxSize;
691+
return this;
692+
}
693+
663694
/**
664695
* Builds a new instance of HttpServletSseServerTransportProvider with the
665696
* configured settings.
@@ -672,7 +703,7 @@ public HttpServletSseServerTransportProvider build() {
672703
}
673704
return new HttpServletSseServerTransportProvider(
674705
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, baseUrl, messageEndpoint,
675-
sseEndpoint, keepAliveInterval, contextExtractor, securityValidator);
706+
sseEndpoint, keepAliveInterval, contextExtractor, securityValidator, requestMaxSize);
676707
}
677708

678709
}

mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
package io.modelcontextprotocol.server.transport;
66

7-
import java.io.BufferedReader;
87
import java.io.IOException;
98
import java.io.PrintWriter;
109
import java.util.List;
@@ -39,6 +38,11 @@
3938
@WebServlet(asyncSupported = true)
4039
public class HttpServletStatelessServerTransport extends HttpServlet implements McpStatelessServerTransport {
4140

41+
/**
42+
* Default maximum size of a single request body: 16 MiB (16 * 1024 * 1024 bytes).
43+
*/
44+
private static final int DEFAULT_REQUEST_MAX_SIZE = 16 * 1024 * 1024;
45+
4246
private static final Logger logger = LoggerFactory.getLogger(HttpServletStatelessServerTransport.class);
4347

4448
public static final String UTF_8 = "UTF-8";
@@ -66,18 +70,37 @@ public class HttpServletStatelessServerTransport extends HttpServlet implements
6670
*/
6771
private final ServerTransportSecurityValidator securityValidator;
6872

73+
/**
74+
* Maximum size, in bytes, of a single request body accepted by this transport.
75+
*/
76+
private final int requestMaxSize;
77+
78+
/**
79+
* Constructs a new HttpServletStatelessServerTransport instance.
80+
* @param jsonMapper The JsonMapper to use for JSON serialization/deserialization of
81+
* messages.
82+
* @param mcpEndpoint The endpoint URI where clients should send their JSON-RPC
83+
* messages.
84+
* @param contextExtractor The extractor for transport context from the request.
85+
* @param securityValidator The security validator for validating HTTP requests.
86+
* @param requestMaxSize The maximum size, in bytes, of a single request body. Must be
87+
* positive.
88+
* @throws IllegalArgumentException if any parameter is null
89+
*/
6990
private HttpServletStatelessServerTransport(McpJsonMapper jsonMapper, String mcpEndpoint,
7091
McpTransportContextExtractor<HttpServletRequest> contextExtractor,
71-
ServerTransportSecurityValidator securityValidator) {
92+
ServerTransportSecurityValidator securityValidator, int requestMaxSize) {
7293
Assert.notNull(jsonMapper, "jsonMapper must not be null");
7394
Assert.notNull(mcpEndpoint, "mcpEndpoint must not be null");
7495
Assert.notNull(contextExtractor, "contextExtractor must not be null");
7596
Assert.notNull(securityValidator, "Security validator must not be null");
97+
Assert.isTrue(requestMaxSize > 0, "requestMaxSize must be positive");
7698

7799
this.jsonMapper = jsonMapper;
78100
this.mcpEndpoint = mcpEndpoint;
79101
this.contextExtractor = contextExtractor;
80102
this.securityValidator = securityValidator;
103+
this.requestMaxSize = requestMaxSize;
81104
}
82105

83106
@Override
@@ -133,6 +156,11 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
133156
return;
134157
}
135158

159+
if (request.getContentLengthLong() > this.requestMaxSize) {
160+
response.sendError(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE);
161+
return;
162+
}
163+
136164
try {
137165
Map<String, List<String>> headers = HttpServletRequestUtils.extractHeaders(request);
138166
this.securityValidator.validateHeaders(headers);
@@ -152,14 +180,9 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
152180
}
153181

154182
try {
155-
BufferedReader reader = request.getReader();
156-
StringBuilder body = new StringBuilder();
157-
String line;
158-
while ((line = reader.readLine()) != null) {
159-
body.append(line);
160-
}
183+
String body = HttpServletRequestUtils.readBody(request, this.requestMaxSize);
161184

162-
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body.toString());
185+
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body);
163186

164187
if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest) {
165188
try {
@@ -201,6 +224,9 @@ else if (message instanceof McpSchema.JSONRPCNotification jsonrpcNotification) {
201224
new McpError("The server accepts either requests or notifications"));
202225
}
203226
}
227+
catch (MaxSizeExceededException e) {
228+
response.sendError(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE);
229+
}
204230
catch (IllegalArgumentException | IOException e) {
205231
logger.error("Failed to deserialize message: {}", e.getMessage());
206232
this.responseError(response, HttpServletResponse.SC_BAD_REQUEST, new McpError("Invalid message format"));
@@ -265,6 +291,8 @@ public static class Builder {
265291

266292
private ServerTransportSecurityValidator securityValidator = ServerTransportSecurityValidator.NOOP;
267293

294+
private int requestMaxSize = DEFAULT_REQUEST_MAX_SIZE;
295+
268296
private Builder() {
269297
// used by a static method
270298
}
@@ -322,6 +350,19 @@ public Builder securityValidator(ServerTransportSecurityValidator securityValida
322350
return this;
323351
}
324352

353+
/**
354+
* Sets the maximum size, in bytes, of a single request body accepted by this
355+
* transport. Requests whose body exceeds this size are rejected with a 413
356+
* (Payload Too Large) response. Defaults to 16 MiB if not set.
357+
* @param requestMaxSize The maximum request body size, in bytes. Must be
358+
* positive.
359+
* @return this builder instance
360+
*/
361+
public Builder maxRequestSize(int requestMaxSize) {
362+
this.requestMaxSize = requestMaxSize;
363+
return this;
364+
}
365+
325366
/**
326367
* Builds a new instance of {@link HttpServletStatelessServerTransport} with the
327368
* configured settings.
@@ -332,7 +373,7 @@ public HttpServletStatelessServerTransport build() {
332373
Assert.notNull(mcpEndpoint, "Message endpoint must be set");
333374
return new HttpServletStatelessServerTransport(
334375
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, mcpEndpoint, contextExtractor,
335-
securityValidator);
376+
securityValidator, requestMaxSize);
336377
}
337378

338379
}

0 commit comments

Comments
 (0)