From fce0f0e112970621d5dbb837d62564631ea7f4b9 Mon Sep 17 00:00:00 2001 From: liutong Date: Tue, 18 Aug 2026 15:38:22 +0000 Subject: [PATCH 1/3] fix: cap WebSocket message size to max_body_size Both the native and mongoose backends read WS messages without a size limit. A malicious client can exhaust server memory: - native: msg.read_all() buffers the entire message - mongoose: Array::make(ws_msg_body_len(), ...) allocates based on the frame header's declared length, which can be arbitrarily large Reuse the existing read_body_limited() for native text/binary messages. For mongoose, reject frames whose declared size exceeds max_body_size before allocating. Co-Authored-By: Claude Opus 4.6 --- mocket.native.mbt | 8 ++++++-- native/mongoose/mongoose.mbt | 8 ++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/mocket.native.mbt b/mocket.native.mbt index b0c6ff0..aad15c1 100644 --- a/mocket.native.mbt +++ b/mocket.native.mbt @@ -455,10 +455,14 @@ async fn handle_websocket_request( let msg = ws.recv() match msg.kind { Text => { - let text = msg.read_all().text() catch { _ => "" } + let data = read_body_limited(msg, mocket.max_body_size) + let text = @utf8.decode_lossy(data) handler(Message(peer, Text(text))) } - Binary => handler(Message(peer, Binary(msg.read_all().binary()))) + Binary => { + let data = read_body_limited(msg, mocket.max_body_size) + handler(Message(peer, Binary(data))) + } } } } catch { diff --git a/native/mongoose/mongoose.mbt b/native/mongoose/mongoose.mbt index bb655ba..a84e609 100644 --- a/native/mongoose/mongoose.mbt +++ b/native/mongoose/mongoose.mbt @@ -104,6 +104,9 @@ let server_map : Map[Int, @mocket.Mocket] = Map([]) ///| let ws_handler_map : Map[Int, @mocket.WebSocketHandler] = Map([]) +///| +let ws_max_body_size : Ref[Int] = Ref(1048576) + ///| fn[T : Show] to_cbytes(s : T) -> Bytes { @utf8.encode(s.to_string()) @@ -158,6 +161,7 @@ fn first_ws_handler() -> @mocket.WebSocketHandler { ///| fn register_ws_handlers(mocket : @mocket.Mocket, port : Int) -> Unit { + ws_max_body_size.val = mocket.max_body_size let mut done = false mocket.ws_static_routes.each(fn(_, handler) { if !done { @@ -266,6 +270,10 @@ pub fn __ws_emit( "message" => handler(Message(peer, Text(from_cbytes(payload)))) "binary" => { let len = ws_msg_body_len() + let max = ws_max_body_size.val + if max > 0 && len > max { + return + } let arr = Array::make(len, b'\x00') let buf = Bytes::from_array(arr) let copied = ws_msg_copy(buf, buf.length()) From 39d167a25c23b3712dfd3992851edc35d0a90c12 Mon Sep 17 00:00:00 2001 From: liutong Date: Tue, 18 Aug 2026 16:17:44 +0000 Subject: [PATCH 2/3] fix: mongoose text messages bypass ws size limit, native dispatches truncated prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Mongoose __ws_emit "message" branch had no size check — text messages of any size reached the handler. Add the same ws_max_body_size guard as the binary branch. 2. Native read_body_limited returns a truncated prefix when a message exceeds max_body_size (e.g. 8192 bytes of a 10000-byte message). Add read_ws_limited that returns None on overflow; the WS handler now silently drops oversized messages instead of dispatching partial data. Co-Authored-By: Claude Opus 4.6 --- mocket.native.mbt | 46 +++++++++++++++++++++++++++++------- native/mongoose/mongoose.mbt | 8 ++++++- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/mocket.native.mbt b/mocket.native.mbt index aad15c1..b60effb 100644 --- a/mocket.native.mbt +++ b/mocket.native.mbt @@ -434,6 +434,32 @@ async fn read_body_limited(reader : &@io.Reader, max_size : Int) -> Bytes { buf.to_bytes() } +///| +async fn read_ws_limited(reader : &@io.Reader, max_size : Int) -> Bytes? { + if max_size <= 0 { + return Some(reader.read_all().binary()) + } + let buf = Buffer() + let chunk_size = 8192 + let chunk = FixedArray::make(chunk_size, b'\x00') + for total = 0 { + let n = reader.read(chunk) catch { _ => break } + if n <= 0 { + break + } + if total + n > max_size { + return None + } + let arr : Array[Byte] = [] + for i = 0; i < n; i = i + 1 { + arr.push(chunk[i]) + } + buf.write_bytes(Bytes::from_array(arr)) + continue total + n + } + Some(buf.to_bytes()) +} + ///| async fn handle_websocket_request( port : Int, @@ -454,15 +480,17 @@ async fn handle_websocket_request( for ;; { let msg = ws.recv() match msg.kind { - Text => { - let data = read_body_limited(msg, mocket.max_body_size) - let text = @utf8.decode_lossy(data) - handler(Message(peer, Text(text))) - } - Binary => { - let data = read_body_limited(msg, mocket.max_body_size) - handler(Message(peer, Binary(data))) - } + Text => + match read_ws_limited(msg, mocket.max_body_size) { + Some(data) => + handler(Message(peer, Text(@utf8.decode_lossy(data)))) + None => () + } + Binary => + match read_ws_limited(msg, mocket.max_body_size) { + Some(data) => handler(Message(peer, Binary(data))) + None => () + } } } } catch { diff --git a/native/mongoose/mongoose.mbt b/native/mongoose/mongoose.mbt index a84e609..9fb9759 100644 --- a/native/mongoose/mongoose.mbt +++ b/native/mongoose/mongoose.mbt @@ -267,7 +267,13 @@ pub fn __ws_emit( ) handler(Open(peer)) } - "message" => handler(Message(peer, Text(from_cbytes(payload)))) + "message" => { + let max = ws_max_body_size.val + if max > 0 && payload.length() > max { + return + } + handler(Message(peer, Text(from_cbytes(payload)))) + } "binary" => { let len = ws_msg_body_len() let max = ws_max_body_size.val From 7fb7317560d619079b0443e4422c90d7ec1bf8bd Mon Sep 17 00:00:00 2001 From: liutong Date: Tue, 18 Aug 2026 16:26:12 +0000 Subject: [PATCH 3/3] fix: read_ws_limited returns None on mid-read error, JS backend enforces max_body_size Co-Authored-By: Claude Opus 4.6 --- mocket.js.mbt | 7 +++++++ mocket.native.mbt | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/mocket.js.mbt b/mocket.js.mbt index 33de5b5..ebc5d1e 100644 --- a/mocket.js.mbt +++ b/mocket.js.mbt @@ -461,6 +461,13 @@ pub fn __ws_emit_js_port( } _ => () } + let max = mocket.max_body_size + if max > 0 && payload.length() > max { + match event_type { + "message" | "binary" => return + _ => () + } + } let peer = WebSocketPeer::{ connection_id, subscribed_channels: [] } dispatch_ws_event(handler, peer, event_type, payload) } diff --git a/mocket.native.mbt b/mocket.native.mbt index b60effb..fb3af10 100644 --- a/mocket.native.mbt +++ b/mocket.native.mbt @@ -443,7 +443,7 @@ async fn read_ws_limited(reader : &@io.Reader, max_size : Int) -> Bytes? { let chunk_size = 8192 let chunk = FixedArray::make(chunk_size, b'\x00') for total = 0 { - let n = reader.read(chunk) catch { _ => break } + let n = reader.read(chunk) catch { _ => return None } if n <= 0 { break }