From 398cfc473aaa8e8af0013f14482b4f2e141ac03b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Helleu?= Date: Mon, 1 Jun 2026 21:56:34 +0200 Subject: [PATCH] relay: limit size of received websocket frame and HTTP body to prevent memory exhaustion A relay client could announce a huge websocket frame (or HTTP body via "Content-Length") and dribble its payload, making WeeChat accumulate it in a buffer that grew without limit, until all memory was exhausted. The websocket frame path is reachable before authentication with the "weechat" and "irc" protocols. The announced websocket frame length and HTTP "Content-Length" are now bounded by WEBSOCKET_FRAME_MAX_LENGTH and RELAY_HTTP_BODY_MAX_LENGTH: an oversized websocket frame closes the connection, and an oversized body is rejected. --- CHANGELOG.md | 1 + src/plugins/relay/relay-http.c | 13 +++++++ src/plugins/relay/relay-http.h | 7 ++++ src/plugins/relay/relay-websocket.c | 8 ++++ src/plugins/relay/relay-websocket.h | 8 ++++ tests/unit/plugins/relay/test-relay-http.cpp | 29 ++++++++++++++ .../plugins/relay/test-relay-websocket.cpp | 39 ++++++++++++++++++- 7 files changed, 104 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2fcabdd5..37aa48688 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed - irc: limit size of data received from the server to prevent memory exhaustion +- relay: limit size of received websocket frame and HTTP body to prevent memory exhaustion - relay: fix timing attack on password authentication ([GHSA-vhv8-g2r9-cwcc](https://github.com/weechat/weechat/security/advisories/GHSA-vhv8-g2r9-cwcc)) - api, relay: fix timing attack on TOTP validation ([GHSA-vhv8-g2r9-cwcc](https://github.com/weechat/weechat/security/advisories/GHSA-vhv8-g2r9-cwcc)) - relay: limit size of decompressed websocket frame with permessage-deflate to prevent memory exhaustion ([GHSA-v2v4-45wm-5cr3](https://github.com/weechat/weechat/security/advisories/GHSA-v2v4-45wm-5cr3)) diff --git a/src/plugins/relay/relay-http.c b/src/plugins/relay/relay-http.c index e60d926e0..8c5e74521 100644 --- a/src/plugins/relay/relay-http.c +++ b/src/plugins/relay/relay-http.c @@ -487,6 +487,19 @@ relay_http_add_to_body (struct t_relay_http_request *request, if (!partial_message || !*partial_message) return; + /* + * reject the body if its announced length is too big: this prevents a + * client from forcing an unbounded allocation by announcing a huge + * "Content-Length" + */ + if (request->content_length > RELAY_HTTP_BODY_MAX_LENGTH) + { + free (*partial_message); + *partial_message = NULL; + request->status = RELAY_HTTP_END; + return; + } + num_bytes_missing = request->content_length - request->body_size; if (num_bytes_missing <= 0) diff --git a/src/plugins/relay/relay-http.h b/src/plugins/relay/relay-http.h index 65f8d46ad..f2c8ed6a4 100644 --- a/src/plugins/relay/relay-http.h +++ b/src/plugins/relay/relay-http.h @@ -53,6 +53,13 @@ enum t_relay_client_http_status #define RELAY_HTTP_ERROR_NOT_FOUND "Resource not found" #define RELAY_HTTP_ERROR_OUT_OF_MEMORY "Out of memory" +/* + * maximum length of an HTTP request body: used as an upper bound on the + * "Content-Length" accepted from a client, to prevent a client from forcing + * an unbounded allocation by announcing a huge body + */ +#define RELAY_HTTP_BODY_MAX_LENGTH (8 * 1024 * 1024) + struct t_relay_http_request { enum t_relay_client_http_status status; /* HTTP status */ diff --git a/src/plugins/relay/relay-websocket.c b/src/plugins/relay/relay-websocket.c index 4e6b9fc1e..497e9922d 100644 --- a/src/plugins/relay/relay-websocket.c +++ b/src/plugins/relay/relay-websocket.c @@ -701,6 +701,14 @@ relay_websocket_decode_frame (const unsigned char *buffer, index_buffer += length_frame_size; } + /* + * reject the frame if its announced length is too big: this prevents + * a client from forcing an unbounded allocation (and unbounded + * accumulation of partial frames) by announcing a huge frame + */ + if (length_frame > WEBSOCKET_FRAME_MAX_LENGTH) + return 0; + if (masked_frame) { /* read mask (4 bytes) */ diff --git a/src/plugins/relay/relay-websocket.h b/src/plugins/relay/relay-websocket.h index 42cf4b822..bf6a9470e 100644 --- a/src/plugins/relay/relay-websocket.h +++ b/src/plugins/relay/relay-websocket.h @@ -39,6 +39,14 @@ #define WEBSOCKET_SUB_PROTOCOL_API_WEECHAT "api.weechat" +/* + * maximum length of a websocket frame received from a client (or a remote + * WeeChat): used as an upper bound on the announced frame payload length, to + * prevent a client from forcing an unbounded allocation by announcing a huge + * frame and dribbling its payload + */ +#define WEBSOCKET_FRAME_MAX_LENGTH (8 * 1024 * 1024) + /* * maximum size of a decompressed websocket frame (with "permessage-deflate"): * used as an upper bound when inflating, to prevent a small compressed frame diff --git a/tests/unit/plugins/relay/test-relay-http.cpp b/tests/unit/plugins/relay/test-relay-http.cpp index 4bb6cadb3..561fff3e4 100644 --- a/tests/unit/plugins/relay/test-relay-http.cpp +++ b/tests/unit/plugins/relay/test-relay-http.cpp @@ -159,6 +159,35 @@ TEST(RelayHttp, RequestAllocReinitFree) relay_http_request_free (request); } +/* + * Tests functions: + * relay_http_add_to_body (body too large is rejected) + */ + +TEST(RelayHttp, AddToBodyLimit) +{ + struct t_relay_http_request *request; + char *partial; + + request = relay_http_request_alloc (); + CHECK(request); + + /* announce a body larger than the maximum allowed */ + request->status = RELAY_HTTP_BODY; + request->content_length = RELAY_HTTP_BODY_MAX_LENGTH + 1; + partial = strdup ("some body data"); + + relay_http_add_to_body (request, &partial); + + /* the body must be rejected: nothing allocated, request ended */ + POINTERS_EQUAL(NULL, request->body); + LONGS_EQUAL(0, request->body_size); + POINTERS_EQUAL(NULL, partial); + LONGS_EQUAL(RELAY_HTTP_END, request->status); + + relay_http_request_free (request); +} + /* * Tests functions: * relay_http_url_decode diff --git a/tests/unit/plugins/relay/test-relay-websocket.cpp b/tests/unit/plugins/relay/test-relay-websocket.cpp index f99e40bcb..eafdb547d 100644 --- a/tests/unit/plugins/relay/test-relay-websocket.cpp +++ b/tests/unit/plugins/relay/test-relay-websocket.cpp @@ -508,7 +508,44 @@ TEST(RelayWebsocket, Inflate) TEST(RelayWebsocket, DecodeFrame) { - /* TODO: write tests */ + struct t_relay_websocket_frame *frames; + char *partial_ws_frame; + int num_frames, partial_ws_frame_size; + /* small unmasked binary frame with payload "hello" */ + unsigned char frame_ok[7] = { 0x82, 0x05, 'h', 'e', 'l', 'l', 'o' }; + /* masked frame announcing a 1 GB payload (64-bit length field) */ + unsigned char frame_too_big[10] = { + 0x82, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, + }; + + /* a valid small frame is decoded */ + frames = NULL; + num_frames = 0; + partial_ws_frame = NULL; + partial_ws_frame_size = 0; + LONGS_EQUAL(1, relay_websocket_decode_frame ( + frame_ok, sizeof (frame_ok), 0, NULL, + &frames, &num_frames, &partial_ws_frame, + &partial_ws_frame_size)); + LONGS_EQUAL(1, num_frames); + CHECK(frames); + LONGS_EQUAL(5, frames[0].payload_size); + MEMCMP_EQUAL("hello", frames[0].payload, 5); + free (frames[0].payload); + free (frames); + free (partial_ws_frame); + + /* a frame announcing an oversized payload is rejected (return 0) */ + frames = NULL; + num_frames = 0; + partial_ws_frame = NULL; + partial_ws_frame_size = 0; + LONGS_EQUAL(0, relay_websocket_decode_frame ( + frame_too_big, sizeof (frame_too_big), 1, NULL, + &frames, &num_frames, &partial_ws_frame, + &partial_ws_frame_size)); + free (frames); + free (partial_ws_frame); } /*