From 3687ce0f0f9dc8e3d4fc7fe9e4bdc31f14134e79 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 8c951fdb2..56bf615f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ SPDX-License-Identifier: GPL-3.0-or-later - fset: remove error displayed in core buffer when clicking with the mouse below the last option displayed - irc: limit size of data received from the server to prevent memory exhaustion - 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)) +- 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)) diff --git a/src/plugins/relay/relay-http.c b/src/plugins/relay/relay-http.c index 7e48eb8f8..e56915566 100644 --- a/src/plugins/relay/relay-http.c +++ b/src/plugins/relay/relay-http.c @@ -513,6 +513,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 f4e7ac5e9..7e74503f0 100644 --- a/src/plugins/relay/relay-http.h +++ b/src/plugins/relay/relay-http.h @@ -57,6 +57,13 @@ enum t_relay_client_http_status #define RELAY_HTTP_ERROR_METHOD_NOT_ALLOWED "Method Not Allowed" #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 cfc294a4a..3295c3a28 100644 --- a/src/plugins/relay/relay-websocket.c +++ b/src/plugins/relay/relay-websocket.c @@ -703,6 +703,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 37b3f8853..523060527 100644 --- a/src/plugins/relay/relay-websocket.h +++ b/src/plugins/relay/relay-websocket.h @@ -41,6 +41,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 f5df9ea48..ee9e5af03 100644 --- a/tests/unit/plugins/relay/test-relay-http.cpp +++ b/tests/unit/plugins/relay/test-relay-http.cpp @@ -161,6 +161,35 @@ TEST(RelayHttp, RequestAllocReinitFree) relay_http_request_free (request); } +/* + * Test 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); +} + /* * Test 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 5f04f9d29..dc44ed3fe 100644 --- a/tests/unit/plugins/relay/test-relay-websocket.cpp +++ b/tests/unit/plugins/relay/test-relay-websocket.cpp @@ -510,7 +510,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); } /*