From 5dbb96b66a406c895b59dc871ba5c8cee9aceead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Helleu?= Date: Sat, 30 May 2026 16:38:42 +0200 Subject: [PATCH] relay: limit size of decompressed websocket frame to prevent memory exhaustion (GHSA-v2v4-45wm-5cr3) An authenticated relay client using the permessage-deflate websocket extension could send a small compressed frame that decompresses to an unbounded amount of data, exhausting all memory and crashing WeeChat. The output buffer in relay_websocket_inflate is now capped to WEBSOCKET_INFLATE_MAX_SIZE: frames decompressing beyond this limit are rejected and the connection is closed. --- CHANGELOG.md | 1 + src/plugins/relay/relay-websocket.c | 26 +++++++++++--- src/plugins/relay/relay-websocket.h | 7 ++++ .../plugins/relay/test-relay-websocket.cpp | 35 +++++++++++++++++++ 4 files changed, 64 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ed6098c5..516be7e2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ SPDX-License-Identifier: GPL-3.0-or-later - core: fix option weechat.look.color_real_white not applied when color is "white" on 16+ colors terminals ([#1742](https://github.com/weechat/weechat/issues/1742)) - irc: fix tag in message with list of names when joining a channel - fset: remove error displayed in core buffer when clicking with the mouse below the last option displayed +- 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)) ## Version 4.9.0 (2026-03-29) diff --git a/src/plugins/relay/relay-websocket.c b/src/plugins/relay/relay-websocket.c index 3403d9a91..cfc294a4a 100644 --- a/src/plugins/relay/relay-websocket.c +++ b/src/plugins/relay/relay-websocket.c @@ -532,7 +532,7 @@ relay_websocket_inflate (const void *data, size_t size, z_stream *strm, int rc; unsigned char append_bytes[4] = { 0x00, 0x00, 0xFF, 0xFF }; Bytef *data2, *dest, *dest2; - uLongf size2, dest_size; + uLongf size2, dest_size, new_size; if (!data || (size == 0) || !strm || !size_decompressed) return NULL; @@ -549,8 +549,13 @@ relay_websocket_inflate (const void *data, size_t size, z_stream *strm, memcpy (data2, data, size); memcpy (data2 + size, append_bytes, sizeof (append_bytes)); - /* estimate the decompressed size, by default 10 * size */ - dest_size = 10 * size2; + /* + * estimate the decompressed size, by default 10 * size, capped to the + * maximum allowed size (also prevents an integer overflow on the + * multiplication) + */ + dest_size = (size2 > WEBSOCKET_INFLATE_MAX_SIZE / 10) ? + WEBSOCKET_INFLATE_MAX_SIZE : 10 * size2; dest = malloc (dest_size); if (!dest) goto error; @@ -579,8 +584,19 @@ relay_websocket_inflate (const void *data, size_t size, z_stream *strm, && (strm->avail_in > 0))) { /* output buffer is not large enough */ - strm->avail_out += dest_size; - dest_size *= 2; + if (dest_size >= WEBSOCKET_INFLATE_MAX_SIZE) + { + /* + * decompressed data is too large: reject the frame + * (protection against a "deflate bomb") + */ + goto error; + } + /* double the buffer, capped to the maximum allowed size */ + new_size = (dest_size > WEBSOCKET_INFLATE_MAX_SIZE / 2) ? + WEBSOCKET_INFLATE_MAX_SIZE : dest_size * 2; + strm->avail_out += (uInt)(new_size - dest_size); + dest_size = new_size; dest2 = realloc (dest, dest_size); if (!dest2) goto error; diff --git a/src/plugins/relay/relay-websocket.h b/src/plugins/relay/relay-websocket.h index ba6225c21..37b3f8853 100644 --- a/src/plugins/relay/relay-websocket.h +++ b/src/plugins/relay/relay-websocket.h @@ -41,6 +41,13 @@ #define WEBSOCKET_SUB_PROTOCOL_API_WEECHAT "api.weechat" +/* + * maximum size of a decompressed websocket frame (with "permessage-deflate"): + * used as an upper bound when inflating, to prevent a small compressed frame + * from decompressing to an unbounded amount of data ("deflate bomb") + */ +#define WEBSOCKET_INFLATE_MAX_SIZE (8 * 1024 * 1024) + struct t_relay_client; struct t_relay_http_request; diff --git a/tests/unit/plugins/relay/test-relay-websocket.cpp b/tests/unit/plugins/relay/test-relay-websocket.cpp index ceaa22078..5f04f9d29 100644 --- a/tests/unit/plugins/relay/test-relay-websocket.cpp +++ b/tests/unit/plugins/relay/test-relay-websocket.cpp @@ -466,6 +466,41 @@ TEST(RelayWebsocket, Inflate) free (payload_comp); relay_websocket_deflate_free (ws_deflate); + + /* + * protection against "deflate bomb": a small compressed frame that + * decompresses to more than WEBSOCKET_INFLATE_MAX_SIZE must be rejected + * (relay_websocket_inflate returns NULL) + */ + ws_deflate = relay_websocket_deflate_alloc (); + CHECK(ws_deflate); + ws_deflate->window_bits_deflate = 15; + ws_deflate->window_bits_inflate = 15; + ws_deflate->strm_deflate = (z_stream *)calloc (1, sizeof (*ws_deflate->strm_deflate)); + CHECK(ws_deflate->strm_deflate); + LONGS_EQUAL(1, relay_websocket_deflate_init_stream_deflate (ws_deflate)); + ws_deflate->strm_inflate = (z_stream *)calloc (1, sizeof (*ws_deflate->strm_inflate)); + CHECK(ws_deflate->strm_inflate); + LONGS_EQUAL(1, relay_websocket_deflate_init_stream_inflate (ws_deflate)); + + /* highly compressible payload that decompresses past the maximum size */ + size_t bomb_size = WEBSOCKET_INFLATE_MAX_SIZE + (1024 * 1024); + char *bomb = (char *)calloc (1, bomb_size); + CHECK(bomb); + + payload_comp = (char *)relay_websocket_deflate (bomb, bomb_size, + ws_deflate->strm_deflate, &size_comp); + CHECK(payload_comp); + CHECK(size_comp < bomb_size); + + payload_decomp = (char *)relay_websocket_inflate (payload_comp, size_comp, + ws_deflate->strm_inflate, &size_decomp); + POINTERS_EQUAL(NULL, payload_decomp); + + free (payload_comp); + free (bomb); + + relay_websocket_deflate_free (ws_deflate); } /*