Bound stdio frame reads with max_line_bytes
## Motivation and Context CRuby's `IO#gets` without a limit reads until the next newline with no upper bound. A peer that never emits a newline can therefore grow a single String until the process is killed by the OS OOM killer. This is a denial-of-service exposure (GHSA-7683-3w9x-ch42), identified during a cross-SDK audit of the same unbounded-buffer pattern across the MCP SDKs. It is most relevant when a host application spawns a sandboxed or untrusted MCP server and pipes its stdout into an unconstrained host process, where a memory-capped peer can exhaust the host. Each frame is now capped at a configurable `max_line_bytes` (default 4 MiB, large enough for any realistic frame including base64-embedded images, and the value recommended in the advisory). A frame that reaches the limit without a newline is treated as a transport error: the client raises, and the server reports the error and closes the connection. A short final frame that ends at EOF without a trailing newline is still accepted, so legitimate traffic is unaffected. Both constructors validate `max_line_bytes:` as a positive Integer, since `nil` or a non-positive value makes `IO#gets` read without an upper bound and would silently disable the protection. ## How Has This Been Tested? Server tests cover the `open` read loop stopping and reporting on an over-limit frame, the `send_request` read loop raising on an over-limit response, and a valid final frame without a trailing newline still being processed. Client tests cover `read_response` raising on an over-limit response frame and a normal response still round-tripping within a custom `max_line_bytes`. Both transports also have tests rejecting invalid `max_line_bytes:` values (`nil`, zero, negative, non-Integer) with an `ArgumentError`. The full test suite and RuboCop pass. ## Breaking Changes None. The default 4 MiB limit is far larger than any realistic JSON-RPC frame, so existing well-behaved traffic is unaffected. The new `max_line_bytes:` keyword on both transport constructors is purely additive and optional.
This commit is contained in:
parent
5518e8db03
commit
267b8fa628
2 changed files with 285 additions and 5 deletions
|
|
@ -65,6 +65,7 @@ stdio_transport.close
|
|||
| `args:` | No | An array of arguments passed to the command. Defaults to `[]`. |
|
||||
| `env:` | No | A hash of environment variables for the server process. Defaults to `nil`. |
|
||||
| `read_timeout:` | No | Timeout in seconds for waiting for a server response. Defaults to `nil`. |
|
||||
| `max_line_bytes:` | No | Maximum byte length of a single newline-delimited response frame. A frame that reaches this limit without a newline is rejected as a transport error, preventing unbounded memory growth from a server that never emits a newline. Defaults to `4 * 1024 * 1024` (4 MiB). |
|
||||
|
||||
## HTTP Transport
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@ transport = MCP::Server::Transports::StdioTransport.new(server)
|
|||
transport.open
|
||||
```
|
||||
|
||||
`StdioTransport.new` accepts an optional `max_line_bytes:` keyword that caps the byte length of a single newline-delimited request frame. A frame that reaches this limit without a newline is rejected and the connection is closed, preventing unbounded memory growth from a peer that never emits a newline. It defaults to `4 * 1024 * 1024` (4 MiB).
|
||||
|
||||
## Streamable HTTP Transport
|
||||
|
||||
`MCP::Server::Transports::StreamableHTTPTransport` is a standard Rack app, so it can be mounted in any Rack-compatible framework.
|
||||
|
|
|
|||
Loading…
Reference in a new issue