mcp: enforce body size limit in Stremable connection (#1055)
This PR enforces a body size limit on Streamable connection to prevent OOM attacks
This commit is contained in:
parent
4c2aece5d7
commit
7f67c851d1
1 changed files with 140 additions and 1 deletions
|
|
@ -194,8 +194,25 @@ type StreamableHTTPOptions struct {
|
|||
// protection := http.NewCrossOriginProtection()
|
||||
// protectedHandler := protection.Handler(handler)
|
||||
CrossOriginProtection *http.CrossOriginProtection
|
||||
|
||||
// MaxRequestBodyBytes limits the number of bytes read from any incoming
|
||||
// HTTP request body. Requests that exceed this limit are rejected with
|
||||
// 413 Request Entity Too Large.
|
||||
//
|
||||
// The limit is enforced during the read, so it applies uniformly to
|
||||
// requests using Content-Length, Transfer-Encoding: chunked, or HTTP/2
|
||||
// (which has no Content-Length).
|
||||
//
|
||||
// If zero, [DefaultMaxRequestBodyBytes] is used.
|
||||
// A negative value disables the limit entirely; do not use this on
|
||||
// servers exposed to untrusted clients.
|
||||
MaxRequestBodyBytes int64
|
||||
}
|
||||
|
||||
// DefaultMaxRequestBodyBytes is the default value used for
|
||||
// [StreamableHTTPOptions.MaxRequestBodyBytes] when it is left at zero.
|
||||
const DefaultMaxRequestBodyBytes = 4 << 20 // 4 MiB
|
||||
|
||||
// NewStreamableHTTPHandler returns a new [StreamableHTTPHandler].
|
||||
//
|
||||
// The getServer function is used to create or look up servers for new
|
||||
|
|
@ -216,6 +233,10 @@ func NewStreamableHTTPHandler(getServer func(*http.Request) *Server, opts *Strea
|
|||
h.opts.CrossOriginProtection = &http.CrossOriginProtection{}
|
||||
}
|
||||
|
||||
if h.opts.MaxRequestBodyBytes == 0 {
|
||||
h.opts.MaxRequestBodyBytes = DefaultMaxRequestBodyBytes
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
|
|
@ -308,6 +329,11 @@ func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Reque
|
|||
}
|
||||
}
|
||||
|
||||
// Bound the request body to protect against OOM attacks.
|
||||
if req.Body != nil && h.opts.MaxRequestBodyBytes > 0 {
|
||||
req.Body = http.MaxBytesReader(w, req.Body, h.opts.MaxRequestBodyBytes)
|
||||
}
|
||||
|
||||
// [§2.7] of the spec (2025-06-18): validate the MCP-Protocol-Version
|
||||
// header. If provided, it must be a supported version. If absent, the
|
||||
// version is unknown (the request may be an initialize for any version).
|
||||
|
|
@ -368,6 +394,11 @@ func (h *StreamableHTTPHandler) serveStateless(w http.ResponseWriter, req *http.
|
|||
|
||||
info, err := h.ephemeralConnectOpts(req)
|
||||
if err != nil {
|
||||
var mbe *http.MaxBytesError
|
||||
if errors.As(err, &mbe) {
|
||||
http.Error(w, fmt.Sprintf("request body exceeds %d bytes", mbe.Limit), http.StatusRequestEntityTooLarge)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
|
@ -435,7 +466,8 @@ func (h *StreamableHTTPHandler) ephemeralConnectOpts(req *http.Request) (*epheme
|
|||
var hasInitialize, hasInitialized, usesNewProtocol, isSubscriptionsListen bool
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read body")
|
||||
// Preserve *http.MaxBytesError so serveStateless can respond with 413.
|
||||
return nil, fmt.Errorf("failed to read body: %w", err)
|
||||
}
|
||||
req.Body.Close()
|
||||
req.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
|
|
@ -1374,6 +1406,11 @@ func (c *streamableServerConn) servePOST(w http.ResponseWriter, req *http.Reques
|
|||
// Read incoming messages.
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
var mbe *http.MaxBytesError
|
||||
if errors.As(err, &mbe) {
|
||||
http.Error(w, fmt.Sprintf("request body exceeds %d bytes", mbe.Limit), http.StatusRequestEntityTooLarge)
|
||||
return
|
||||
}
|
||||
http.Error(w, "failed to read body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue