From d8455327bf00f1b3342049f6bb7f4cd96ae7d69f Mon Sep 17 00:00:00 2001 From: liutong Date: Tue, 18 Aug 2026 01:04:46 +0000 Subject: [PATCH] fix(security): enforce max request body size to prevent memory exhaustion (CWE-400) Add configurable `max_body_size` (default 1MB) to `Mocket` to reject oversized HTTP request bodies before they are buffered into memory. - Native backend: check Content-Length header upfront and enforce a streaming read limit via `read_body_limited()`; return 413 when exceeded. - JS backend: track cumulative chunk size in the `data` callback and return 413 when the limit is crossed. Without this fix an unauthenticated attacker can send a single POST with an arbitrarily large body, causing the server to allocate until OOM. Co-Authored-By: Claude Opus 4.6 --- index.mbt | 4 +++- mocket.js.mbt | 17 ++++++++++++++++- mocket.native.mbt | 40 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/index.mbt b/index.mbt index a1f1b60..8f7a151 100644 --- a/index.mbt +++ b/index.mbt @@ -23,10 +23,11 @@ pub(all) struct Mocket { ws_clients : Map[String, Unit] ws_channels : Map[String, Map[String, Unit]] ws_client_port : Map[String, Int] + max_body_size : Int } ///| -pub fn new(base_path? : String = "") -> Mocket { +pub fn new(base_path? : String = "", max_body_size? : Int = 1048576) -> Mocket { { base_path, mappings: {}, @@ -40,6 +41,7 @@ pub fn new(base_path? : String = "") -> Mocket { ws_clients: {}, ws_channels: {}, ws_client_port: {}, + max_body_size, } } diff --git a/mocket.js.mbt b/mocket.js.mbt index ba579be..169f370 100644 --- a/mocket.js.mbt +++ b/mocket.js.mbt @@ -340,13 +340,28 @@ pub fn listen_ffi(mocket : Mocket, address : String) -> Unit { // 如果是 post,先等待 data 事件 if event.req.http_method == "POST" { let buffer = Buffer() + let mut total_size = 0 + let mut exceeded = false (try? suspend(fn(res, _) { req.on("data", data => { - buffer.write_bytes(node_body_chunk_to_bytes(data)) + if !exceeded { + let chunk = node_body_chunk_to_bytes(data) + total_size = total_size + chunk.length() + if mocket.max_body_size > 0 && total_size > mocket.max_body_size { + exceeded = true + } else { + buffer.write_bytes(chunk) + } + } }) req.on("end", _ => res(())) })) |> ignore + if exceeded { + res.write_head(413, @js.Object::new().to_value()) + res.end(@js.Value::cast_from("Request body too large")) + return + } event.req.raw_body = buffer.to_bytes() } diff --git a/mocket.native.mbt b/mocket.native.mbt index 93e304f..0896045 100644 --- a/mocket.native.mbt +++ b/mocket.native.mbt @@ -382,7 +382,19 @@ async fn handle_http_request( conn : @http.ServerConnection, ) -> Unit { let raw_body = if request_has_body(request) { - body_reader.read_all().binary() + let content_length = request.headers + .get("content-length") + .map(s => try { @string.parse_int(s.trim()) } catch { _ => 0 }) + .unwrap_or(0) + if mocket.max_body_size > 0 && content_length > mocket.max_body_size { + let err_response = HttpResponse::new( + RequestEntityTooLarge, + raw_body=b"Request body too large", + ) + send_native_response(request, conn, err_response) + return + } + read_body_limited(body_reader, mocket.max_body_size) } else { b"" } @@ -396,6 +408,32 @@ async fn handle_http_request( send_native_response(request, conn, response) } +///| +async fn read_body_limited(reader : &@io.Reader, max_size : Int) -> Bytes { + if max_size <= 0 { + return reader.read_all().binary() + } + let buf = Buffer() + let chunk_size = 8192 + let chunk = FixedArray::make(chunk_size, b'\x00') + for total = 0 { + let n = try { reader.read(chunk) } catch { _ => break } + if n <= 0 { + break + } + if total + n > max_size { + break + } + 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 + } + buf.to_bytes() +} + ///| async fn handle_websocket_request( port : Int,