From 6cac7c6e123985c567bc2a2b46abc581161de757 Mon Sep 17 00:00:00 2001 From: Zayd Rajab Date: Thu, 16 Jul 2026 02:32:19 +0000 Subject: [PATCH] websocket: Reject oversized control frames RFC 6455 section 5.5 limits control frame payloads to 125 bytes. Reject extended payload length indicators as soon as the base header is available, before waiting for additional length or payload bytes. Add regression coverage for both extended length indicators and both receive directions. This is CVE-2026-15711. Closes #515 --- libsoup/websocket/soup-websocket-connection.c | 7 ++ tests/websocket-test.c | 113 ++++++++++++++++++ 2 files changed, 120 insertions(+) diff --git a/libsoup/websocket/soup-websocket-connection.c b/libsoup/websocket/soup-websocket-connection.c index 2f031f1d..daee6196 100644 --- a/libsoup/websocket/soup-websocket-connection.c +++ b/libsoup/websocket/soup-websocket-connection.c @@ -1088,6 +1088,13 @@ process_frame (SoupWebsocketConnection *self) return FALSE; } + /* RFC 6455 section 5.5 limits control frame payloads to 125 bytes. */ + if (control && (header[1] & 0x7f) > 125) { + g_debug ("received oversized control frame"); + protocol_error_and_close (self); + return FALSE; + } + switch (header[1] & 0x7f) { case 126: /* If 126, the following 2 bytes interpreted as a 16-bit diff --git a/tests/websocket-test.c b/tests/websocket-test.c index c33398c4..1b810f63 100644 --- a/tests/websocket-test.c +++ b/tests/websocket-test.c @@ -839,6 +839,98 @@ test_send_bad_data (Test *test, g_assert_cmpuint (soup_websocket_connection_get_close_code (test->client), ==, SOUP_WEBSOCKET_CLOSE_BAD_DATA); } +static gboolean +on_timeout_set_flag (gpointer user_data) +{ + gboolean *timed_out = user_data; + + *timed_out = TRUE; + + return G_SOURCE_REMOVE; +} + +static void +wait_for_websocket_error (GError **error) +{ + gboolean timed_out = FALSE; + guint timeout_id; + + timeout_id = g_timeout_add_seconds (1, on_timeout_set_flag, &timed_out); + WAIT_UNTIL (*error != NULL || timed_out); + + if (!timed_out) + g_source_remove (timeout_id); + g_assert_false (timed_out); +} + +static void +write_oversized_control_frame_header (GIOStream *io, + guint8 length_indicator, + gboolean masked) +{ + guint8 frame[] = { 0x8a, length_indicator }; + GError *error = NULL; + gsize written; + + g_assert_true (length_indicator == 126 || length_indicator == 127); + + if (masked) + frame[1] |= 0x80; + + g_output_stream_write_all (g_io_stream_get_output_stream (io), + frame, sizeof (frame), &written, NULL, &error); + g_assert_no_error (error); + g_assert_cmpuint (written, ==, sizeof (frame)); +} + +typedef struct { + guint8 length_indicator; + gboolean server_receives; +} OversizedControlFrameTest; + +static void +test_receive_oversized_control_frame (Test *test, + gconstpointer data) +{ + const OversizedControlFrameTest *config = data; + SoupWebsocketConnection *receiver; + SoupWebsocketConnection *sender; + GError *error = NULL; + GIOStream *io; + gulong error_id; + gboolean close_event = FALSE; + + if (config->server_receives) { + receiver = test->server; + sender = test->client; + } else { + receiver = test->client; + sender = test->server; + } + + g_signal_handlers_disconnect_by_func (receiver, on_error_not_reached, NULL); + error_id = g_signal_connect (receiver, "error", G_CALLBACK (on_error_copy), &error); + g_signal_connect (sender, "closed", G_CALLBACK (on_close_set_flag), &close_event); + + io = soup_websocket_connection_get_io_stream (sender); + write_oversized_control_frame_header (io, config->length_indicator, + config->server_receives); + wait_for_websocket_error (&error); + g_assert_error (error, SOUP_WEBSOCKET_ERROR, SOUP_WEBSOCKET_CLOSE_PROTOCOL_ERROR); + g_clear_error (&error); + g_signal_handler_disconnect (receiver, error_id); + + WAIT_UNTIL (soup_websocket_connection_get_state (sender) == SOUP_WEBSOCKET_STATE_CLOSED); + g_assert_true (close_event); + g_assert_cmpuint (soup_websocket_connection_get_close_code (sender), ==, + SOUP_WEBSOCKET_CLOSE_PROTOCOL_ERROR); +} + +static const OversizedControlFrameTest oversized_control_frame_16_server = { 126, TRUE }; +static const OversizedControlFrameTest oversized_control_frame_64_server = { 127, TRUE }; +static const OversizedControlFrameTest oversized_control_frame_16_client = { 126, FALSE }; +static const OversizedControlFrameTest oversized_control_frame_64_client = { 127, FALSE }; + static const char *negotiate_client_protocols[] = { "bbb", "ccc", NULL }; static const char *negotiate_server_protocols[] = { "aaa", "bbb", "ccc", NULL }; static const char *negotiated_protocol = "bbb"; @@ -2580,6 +2672,27 @@ main (int argc, test_send_bad_data, teardown_soup_connection); + g_test_add ("/websocket/direct/server-receive-oversized-control-frame/16-bit", + Test, &oversized_control_frame_16_server, + setup_direct_connection, + test_receive_oversized_control_frame, + teardown_direct_connection); + g_test_add ("/websocket/direct/server-receive-oversized-control-frame/64-bit", + Test, &oversized_control_frame_64_server, + setup_direct_connection, + test_receive_oversized_control_frame, + teardown_direct_connection); + g_test_add ("/websocket/direct/client-receive-oversized-control-frame/16-bit", + Test, &oversized_control_frame_16_client, + setup_direct_connection, + test_receive_oversized_control_frame, + teardown_direct_connection); + g_test_add ("/websocket/direct/client-receive-oversized-control-frame/64-bit", + Test, &oversized_control_frame_64_client, + setup_direct_connection, + test_receive_oversized_control_frame, + teardown_direct_connection); + g_test_add ("/websocket/direct/close-clean-client", Test, NULL, NULL, test_close_clean_client_direct, NULL); -- GitLab