public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
From: Adrian Vovk <avovk@redhat.com>
To: git-commits@fedoraproject.org
Subject: [rpms/libsoup3] f45: Apply various CVE fixes
Date: Sat, 05 Sep 2026 01:22:54 GMT [thread overview]
Message-ID: <178857137424.1.6571316064283254185.rpms-libsoup3-f2b42d21480e@fedoraproject.org> (raw)
A new commit has been pushed.
Repo : rpms/libsoup3
Branch : f45
Commit : f2b42d21480e67966605f0b232c023b13a50cde4
Author : Adrian Vovk <avovk@redhat.com>
Date : 2026-09-04T21:18:30-04:00
Stats : +1949/-0 in 7 file(s)
URL : https://src.fedoraproject.org/rpms/libsoup3/c/f2b42d21480e67966605f0b232c023b13a50cde4?branch=f45
Log:
Apply various CVE fixes
---
diff --git a/CVE-2026-15709.patch b/CVE-2026-15709.patch
new file mode 100644
index 0000000..799c378
--- /dev/null
+++ b/CVE-2026-15709.patch
@@ -0,0 +1,782 @@
+From a4294044466df66ea98492d67bb74172ed3f8762 Mon Sep 17 00:00:00 2001
+From: Zayd Rajab <zaydr@amazon.com>
+Date: Fri, 4 Sep 2026 14:17:23 -0500
+Subject: [PATCH] websocket: Bound decompressed message size during inflation
+
+Pass each connection's remaining max-total-message-size allowance through
+the generic WebSocket extension processing path via a new
+process_incoming_message_with_limit vfunc, falling back to
+process_incoming_message for extensions that don't implement it. Bound
+permessage-deflate output growth to that allowance and use a one-byte
+probe to detect output beyond an exact boundary without allocating
+another normal output chunk.
+
+Preserve the documented unlimited default for clients and direct
+connections. SoupServer continues applying its existing 128 KiB default,
+while callers can configure the limit explicitly in either receive
+direction.
+
+Add boundary, generic-extension fallback, receive-direction, SoupServer,
+fragmentation, and unlimited-mode coverage.
+
+This addresses CVE-2026-15709.
+
+Reported-by: Tristan Madani <secientist@gmail.com>
+Closes !543
+---
+ libsoup/websocket/soup-websocket-connection.c | 34 +-
+ .../soup-websocket-extension-deflate.c | 144 ++++++---
+ libsoup/websocket/soup-websocket-extension.c | 53 +++
+ libsoup/websocket/soup-websocket-extension.h | 14 +-
+ tests/websocket-test.c | 306 +++++++++++++++++-
+ 5 files changed, 506 insertions(+), 45 deletions(-)
+
+diff --git a/libsoup/websocket/soup-websocket-connection.c b/libsoup/websocket/soup-websocket-connection.c
+index ffd713ef..f07d4219 100644
+--- a/libsoup/websocket/soup-websocket-connection.c
++++ b/libsoup/websocket/soup-websocket-connection.c
+@@ -990,8 +990,15 @@ process_contents (SoupWebsocketConnection *self,
+ case 0x02:
+ /* Safety valve */
+ if (priv->max_total_message_size > 0 &&
+- (priv->message_data->len + payload_len) > priv->max_total_message_size) {
+- too_big_message_error_and_close (self, (priv->message_data->len + payload_len));
++ (priv->message_data->len > priv->max_total_message_size ||
++ payload_len > priv->max_total_message_size - priv->message_data->len)) {
++ guint64 message_size = priv->message_data->len;
++
++ if (payload_len > G_MAXUINT64 - message_size)
++ message_size = G_MAXUINT64;
++ else
++ message_size += payload_len;
++ too_big_message_error_and_close (self, message_size);
+ return;
+ }
+ g_byte_array_append (priv->message_data, payload, payload_len);
+@@ -1036,6 +1043,21 @@ process_contents (SoupWebsocketConnection *self,
+ }
+ }
+
++static guint64
++get_remaining_message_size (SoupWebsocketConnectionPrivate *priv)
++{
++ if (priv->max_total_message_size == 0)
++ return G_MAXUINT64;
++
++ if (!priv->message_data)
++ return priv->max_total_message_size;
++
++ if (priv->message_data->len >= priv->max_total_message_size)
++ return 0;
++
++ return priv->max_total_message_size - priv->message_data->len;
++}
++
+ static gboolean
+ process_frame (SoupWebsocketConnection *self)
+ {
+@@ -1051,6 +1073,7 @@ process_frame (SoupWebsocketConnection *self)
+ gsize len;
+ gsize at;
+ GBytes *filtered_bytes;
++ guint64 max_output_size;
+ GList *l;
+ GError *error = NULL;
+
+@@ -1165,11 +1188,16 @@ process_frame (SoupWebsocketConnection *self)
+ }
+
+ filtered_bytes = g_bytes_new_static (payload, payload_len);
++ max_output_size = get_remaining_message_size (priv);
+ for (l = priv->extensions; l != NULL; l = g_list_next (l)) {
+ SoupWebsocketExtension *extension;
+
+ extension = (SoupWebsocketExtension *)l->data;
+- filtered_bytes = soup_websocket_extension_process_incoming_message (extension, priv->incoming->data, filtered_bytes, &error);
++ filtered_bytes = soup_websocket_extension_process_incoming_message_with_limit (extension,
++ priv->incoming->data,
++ filtered_bytes,
++ max_output_size,
++ &error);
+ if (error) {
+ emit_error_and_close (self, error, FALSE);
+ return FALSE;
+diff --git a/libsoup/websocket/soup-websocket-extension-deflate.c b/libsoup/websocket/soup-websocket-extension-deflate.c
+index fa98a2e2..2c649a88 100644
+--- a/libsoup/websocket/soup-websocket-extension-deflate.c
++++ b/libsoup/websocket/soup-websocket-extension-deflate.c
+@@ -359,19 +359,29 @@ soup_websocket_extension_deflate_process_outgoing_message (SoupWebsocketExtensio
+ return g_byte_array_free_to_bytes (buffer);
+ }
+
++static void
++inflater_reset (Inflater *inflater)
++{
++ inflateReset (&inflater->zstream);
++ inflater->uncompress_ongoing = FALSE;
++}
++
+ static GBytes *
+-soup_websocket_extension_deflate_process_incoming_message (SoupWebsocketExtension *extension,
+- guint8 *header,
+- GBytes *payload,
+- GError **error)
++soup_websocket_extension_deflate_process_incoming_message_with_limit (SoupWebsocketExtension *extension,
++ guint8 *header,
++ GBytes *payload,
++ guint64 max_output_size,
++ GError **error)
+ {
+ const guint8 *payload_data;
+ gsize payload_length;
+ gboolean fin, control, compressed;
+ GByteArray *buffer;
+- gsize bytes_read, bytes_written;
++ gsize input_offset, bytes_written;
+ int result;
+ gboolean tail_added = FALSE;
++ gboolean using_limit_probe = FALSE;
++ guint8 limit_probe;
+ SoupWebsocketExtensionDeflatePrivate *priv;
+
+ priv = soup_websocket_extension_deflate_get_instance_private (SOUP_WEBSOCKET_EXTENSION_DEFLATE (extension));
+@@ -394,6 +404,7 @@ soup_websocket_extension_deflate_process_incoming_message (SoupWebsocketExtensio
+ SOUP_WEBSOCKET_ERROR,
+ SOUP_WEBSOCKET_CLOSE_PROTOCOL_ERROR,
+ "Received a non-first frame with RSV1 flag set");
++ inflater_reset (&priv->inflater);
+ g_bytes_unref (payload);
+ return NULL;
+ }
+@@ -410,64 +421,122 @@ soup_websocket_extension_deflate_process_incoming_message (SoupWebsocketExtensio
+
+ buffer = g_byte_array_new ();
+
+- bytes_read = 0;
+- priv->inflater.zstream.next_in = (void *)payload_data;
+- priv->inflater.zstream.avail_in = payload_length;
+-
++ input_offset = 0;
++ priv->inflater.zstream.avail_in = 0;
+ bytes_written = 0;
+ priv->inflater.zstream.avail_out = 0;
+
+- do {
+- gsize read_remaining;
+- gsize write_remaining;
++ while (TRUE) {
++ uInt input_before, output_before;
++ uInt input_consumed, output_produced;
+
+- if (priv->inflater.zstream.avail_out == 0) {
+- guint current_position;
++ if (priv->inflater.zstream.avail_in == 0 && input_offset < payload_length) {
++ gsize input_length = MIN (payload_length - input_offset, G_MAXUINT);
+
+- priv->inflater.zstream.avail_out = BUFFER_SIZE;
+- current_position = buffer->len;
+- g_byte_array_set_size (buffer, buffer->len + BUFFER_SIZE);
+- priv->inflater.zstream.next_out = buffer->data + current_position;
++ priv->inflater.zstream.next_in = (void *)(payload_data + input_offset);
++ priv->inflater.zstream.avail_in = (uInt)input_length;
++ input_offset += input_length;
+ }
+
+- if (priv->inflater.zstream.avail_in == 0 && !tail_added && fin) {
++ if (priv->inflater.zstream.avail_in == 0 &&
++ input_offset == payload_length &&
++ !tail_added && fin) {
+ /* Append 4 octets of 0x00 0x00 0xff 0xff to the tail end */
+ priv->inflater.zstream.next_in = (void *)"\x00\x00\xff\xff";
+ priv->inflater.zstream.avail_in = 4;
+- bytes_read = 0;
+ tail_added = TRUE;
+ }
+
+- read_remaining = tail_added ? 4 : payload_length - bytes_read;
+- write_remaining = buffer->len - bytes_written;
++ if (priv->inflater.zstream.avail_out == 0) {
++ guint output_length = BUFFER_SIZE;
++
++ if (bytes_written >= max_output_size || buffer->len == G_MAXUINT) {
++ priv->inflater.zstream.next_out = &limit_probe;
++ priv->inflater.zstream.avail_out = 1;
++ using_limit_probe = TRUE;
++ } else {
++ guint64 remaining = max_output_size - bytes_written;
++ guint current_position = buffer->len;
++
++ output_length = MIN ((guint64)output_length, remaining);
++ output_length = MIN (output_length, G_MAXUINT - buffer->len);
++ g_assert (output_length > 0);
++
++ g_byte_array_set_size (buffer, buffer->len + output_length);
++ priv->inflater.zstream.next_out = buffer->data + current_position;
++ priv->inflater.zstream.avail_out = output_length;
++ using_limit_probe = FALSE;
++ }
++ }
++
++ input_before = priv->inflater.zstream.avail_in;
++ output_before = priv->inflater.zstream.avail_out;
+ result = inflate (&priv->inflater.zstream, tail_added ? Z_FINISH : Z_NO_FLUSH);
+- bytes_read += read_remaining - priv->inflater.zstream.avail_in;
+- bytes_written += write_remaining - priv->inflater.zstream.avail_out;
++ input_consumed = input_before - priv->inflater.zstream.avail_in;
++ output_produced = output_before - priv->inflater.zstream.avail_out;
++
++ if (using_limit_probe && output_produced > 0)
++ goto output_too_large;
++
++ bytes_written += output_produced;
++
+ if (!tail_added && result == Z_STREAM_END) {
+ /* Received a block with BFINAL set to 1. Reset decompression state. */
+ result = inflateReset (&priv->inflater.zstream);
+ }
+
+- if ((!fin && bytes_read == payload_length) || (fin && tail_added && bytes_read == 4))
+- break;
+- } while (result == Z_OK || result == Z_BUF_ERROR);
+-
+- g_bytes_unref (payload);
++ if (result != Z_OK && result != Z_BUF_ERROR)
++ goto invalid_data;
+
+- if (result != Z_OK && result != Z_BUF_ERROR) {
+- priv->inflater.uncompress_ongoing = FALSE;
+- g_set_error_literal (error,
+- SOUP_WEBSOCKET_ERROR,
+- SOUP_WEBSOCKET_CLOSE_PROTOCOL_ERROR,
+- "Failed to uncompress incoming frame");
+- g_byte_array_unref (buffer);
++ if (priv->inflater.zstream.avail_in == 0 &&
++ input_offset == payload_length &&
++ ((!fin && !tail_added) || (fin && tail_added)) &&
++ priv->inflater.zstream.avail_out > 0)
++ break;
+
+- return NULL;
++ if (input_consumed == 0 && output_produced == 0)
++ goto invalid_data;
+ }
+
++ g_bytes_unref (payload);
+ g_byte_array_set_size (buffer, bytes_written);
+
+ return g_byte_array_free_to_bytes (buffer);
++
++output_too_large:
++ inflater_reset (&priv->inflater);
++ g_bytes_unref (payload);
++ g_byte_array_unref (buffer);
++ g_set_error_literal (error,
++ SOUP_WEBSOCKET_ERROR,
++ SOUP_WEBSOCKET_CLOSE_TOO_BIG,
++ "Decompressed WebSocket message exceeds configured maximum size");
++
++ return NULL;
++
++invalid_data:
++ inflater_reset (&priv->inflater);
++ g_bytes_unref (payload);
++ g_byte_array_unref (buffer);
++ g_set_error_literal (error,
++ SOUP_WEBSOCKET_ERROR,
++ SOUP_WEBSOCKET_CLOSE_PROTOCOL_ERROR,
++ "Failed to uncompress incoming frame");
++
++ return NULL;
++}
++
++static GBytes *
++soup_websocket_extension_deflate_process_incoming_message (SoupWebsocketExtension *extension,
++ guint8 *header,
++ GBytes *payload,
++ GError **error)
++{
++ return soup_websocket_extension_deflate_process_incoming_message_with_limit (extension,
++ header,
++ payload,
++ G_MAXUINT64,
++ error);
+ }
+
+ static void
+@@ -483,6 +552,7 @@ soup_websocket_extension_deflate_class_init (SoupWebsocketExtensionDeflateClass
+ extension_class->get_response_params = soup_websocket_extension_deflate_get_response_params;
+ extension_class->process_outgoing_message = soup_websocket_extension_deflate_process_outgoing_message;
+ extension_class->process_incoming_message = soup_websocket_extension_deflate_process_incoming_message;
++ extension_class->process_incoming_message_with_limit = soup_websocket_extension_deflate_process_incoming_message_with_limit;
+
+ object_class->finalize = soup_websocket_extension_deflate_finalize;
+ }
+diff --git a/libsoup/websocket/soup-websocket-extension.c b/libsoup/websocket/soup-websocket-extension.c
+index 8882d2c0..95719e8c 100644
+--- a/libsoup/websocket/soup-websocket-extension.c
++++ b/libsoup/websocket/soup-websocket-extension.c
+@@ -47,6 +47,9 @@
+ * before it's sent. Reserved bits of the header should be changed.
+ * @process_incoming_message: called to process the payload data of a message
+ * after it's received. Reserved bits of the header should be cleared.
++ * @process_incoming_message_with_limit: called to process the payload data of
++ * a message after it's received without exceeding the requested maximum
++ * output size. If unset, @process_incoming_message is used. Since 3.8
+ *
+ * The class structure for the [class@WebsocketExtension].
+ */
+@@ -216,3 +219,53 @@ soup_websocket_extension_process_incoming_message (SoupWebsocketExtension *exten
+
+ return klass->process_incoming_message (extension, header, payload, error);
+ }
++
++/**
++ * soup_websocket_extension_process_incoming_message_with_limit:
++ * @extension: a #SoupWebsocketExtension
++ * @header: (inout): the message header
++ * @payload: (transfer full): the payload data
++ * @max_output_size: the maximum size in bytes of the processed payload
++ * @error: return location for a #GError
++ *
++ * Process a message after it's received, without producing more than
++ * @max_output_size bytes of output.
++ *
++ * This behaves like [method@WebsocketExtension.process_incoming_message],
++ * but extensions that expand their input (such as `permessage-deflate`)
++ * stop and return an error with [error@WebsocketError.CLOSE_TOO_BIG]
++ * instead of producing output larger than @max_output_size. Extensions
++ * that don't implement this fall back to
++ * [method@WebsocketExtension.process_incoming_message].
++ *
++ * Returns: (transfer full): the message payload data, or %NULL in case of error
++ *
++ * Since: 3.8
++ */
++GBytes *
++soup_websocket_extension_process_incoming_message_with_limit (SoupWebsocketExtension *extension,
++ guint8 *header,
++ GBytes *payload,
++ guint64 max_output_size,
++ GError **error)
++{
++ SoupWebsocketExtensionClass *klass;
++
++ g_return_val_if_fail (SOUP_IS_WEBSOCKET_EXTENSION (extension), NULL);
++ g_return_val_if_fail (header != NULL, NULL);
++ g_return_val_if_fail (payload != NULL, NULL);
++ g_return_val_if_fail (error == NULL || *error == NULL, NULL);
++
++ klass = SOUP_WEBSOCKET_EXTENSION_GET_CLASS (extension);
++ if (!klass->process_incoming_message_with_limit)
++ return soup_websocket_extension_process_incoming_message (extension,
++ header,
++ payload,
++ error);
++
++ return klass->process_incoming_message_with_limit (extension,
++ header,
++ payload,
++ max_output_size,
++ error);
++}
+diff --git a/libsoup/websocket/soup-websocket-extension.h b/libsoup/websocket/soup-websocket-extension.h
+index 1931a264..a9d5e35c 100644
+--- a/libsoup/websocket/soup-websocket-extension.h
++++ b/libsoup/websocket/soup-websocket-extension.h
+@@ -55,8 +55,14 @@ struct _SoupWebsocketExtensionClass {
+ GBytes *payload,
+ GError **error);
+
++ GBytes *(* process_incoming_message_with_limit) (SoupWebsocketExtension *extension,
++ guint8 *header,
++ GBytes *payload,
++ guint64 max_output_size,
++ GError **error);
++
+ /* <private> */
+- gpointer padding[6];
++ gpointer padding[5];
+ };
+
+ SOUP_AVAILABLE_IN_ALL
+@@ -80,5 +86,11 @@ GBytes *soup_websocket_extension_process_incoming_message (Soup
+ guint8 *header,
+ GBytes *payload,
+ GError **error);
++SOUP_AVAILABLE_IN_3_8
++GBytes *soup_websocket_extension_process_incoming_message_with_limit (SoupWebsocketExtension *extension,
++ guint8 *header,
++ GBytes *payload,
++ guint64 max_output_size,
++ GError **error);
+
+ G_END_DECLS
+diff --git a/tests/websocket-test.c b/tests/websocket-test.c
+index db088a55..5f94b5f6 100644
+--- a/tests/websocket-test.c
++++ b/tests/websocket-test.c
+@@ -73,6 +73,18 @@ on_error_copy (SoupWebsocketConnection *ws,
+ *copy = g_error_copy (error);
+ }
+
++static void
++on_error_copy_once (SoupWebsocketConnection *ws,
++ GError *error,
++ gpointer user_data)
++{
++ GError **copy = user_data;
++
++ g_assert_null (*copy);
++ *copy = g_error_copy (error);
++ g_signal_handlers_disconnect_by_func (ws, G_CALLBACK (on_error_copy_once), user_data);
++}
++
+ static void
+ setup_listener (Test *test)
+ {
+@@ -649,7 +661,7 @@ test_send_big_packets_soup (Test *test,
+ g_assert_cmpuint (soup_websocket_connection_get_max_incoming_payload_size (test->client), ==, 128 * 1024);
+ g_assert_cmpuint (soup_websocket_connection_get_max_total_message_size (test->client), ==, 0);
+
+- /* Max total message size defaults to 0 (unlimited), but SoupServer applies its own limit by default. */
++ /* SoupServer applies its own total message size limit by default. */
+ g_assert_cmpuint (soup_websocket_connection_get_max_incoming_payload_size (test->server), ==, 128 * 1024);
+ g_assert_cmpuint (soup_websocket_connection_get_max_total_message_size (test->server), ==, 128 * 1024);
+
+@@ -756,9 +768,7 @@ test_send_exceeding_server_max_message_size (Test *test,
+ soup_websocket_connection_set_max_total_message_size (test->client, 0);
+ g_assert_cmpuint (soup_websocket_connection_get_max_total_message_size (test->client), ==, 0);
+
+- /* Set the server message total message size manually, because its
+- * default is different for direct connection vs. soup connection.
+- */
++ /* The direct server connection defaults to unlimited. */
+ soup_websocket_connection_set_max_total_message_size (test->server, 128 * 1024);
+ g_assert_cmpuint (soup_websocket_connection_get_max_total_message_size (test->server), ==, 128 * 1024);
+
+@@ -1670,6 +1680,222 @@ send_fragments_server_thread (gpointer user_data)
+ return NULL;
+ }
+
++typedef struct {
++ SoupWebsocketExtension parent_instance;
++} LegacyWebsocketExtension;
++
++typedef struct {
++ SoupWebsocketExtensionClass parent_class;
++} LegacyWebsocketExtensionClass;
++
++GType legacy_websocket_extension_get_type (void);
++
++#define LEGACY_TYPE_WEBSOCKET_EXTENSION (legacy_websocket_extension_get_type ())
++G_DEFINE_TYPE (LegacyWebsocketExtension, legacy_websocket_extension, SOUP_TYPE_WEBSOCKET_EXTENSION)
++
++static gboolean legacy_extension_processed;
++
++static GBytes *
++legacy_websocket_extension_process_incoming_message (SoupWebsocketExtension *extension,
++ guint8 *header,
++ GBytes *payload,
++ GError **error)
++{
++ legacy_extension_processed = TRUE;
++
++ return payload;
++}
++
++static void
++legacy_websocket_extension_class_init (LegacyWebsocketExtensionClass *klass)
++{
++ SoupWebsocketExtensionClass *extension_class = SOUP_WEBSOCKET_EXTENSION_CLASS (klass);
++
++ extension_class->process_incoming_message = legacy_websocket_extension_process_incoming_message;
++}
++
++static void
++legacy_websocket_extension_init (LegacyWebsocketExtension *extension)
++{
++}
++
++static void
++test_websocket_extension_limit_fallback (void)
++{
++ SoupWebsocketExtension *extension;
++ GBytes *payload;
++ GBytes *output;
++ GError *error = NULL;
++ guint8 header = 0x82;
++
++ legacy_extension_processed = FALSE;
++ extension = g_object_new (LEGACY_TYPE_WEBSOCKET_EXTENSION, NULL);
++ payload = g_bytes_new_static ("x", 1);
++ output = soup_websocket_extension_process_incoming_message_with_limit (extension,
++ &header,
++ payload,
++ 1,
++ &error);
++
++ g_assert_no_error (error);
++ g_assert_true (legacy_extension_processed);
++ g_assert_true (output == payload);
++
++ g_bytes_unref (output);
++ g_object_unref (extension);
++}
++
++typedef struct {
++ gsize output_size;
++ guint64 max_output_size;
++ gboolean expect_too_big;
++} DeflateOutputLimitTest;
++
++static void
++test_deflate_output_limit (gconstpointer data)
++{
++ const DeflateOutputLimitTest *config = data;
++ SoupWebsocketExtension *sender;
++ SoupWebsocketExtension *receiver;
++ GBytes *compressed;
++ GBytes *output;
++ GError *error = NULL;
++ guint8 header = 0x82;
++
++ sender = g_object_new (SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE, NULL);
++ g_assert_true (soup_websocket_extension_configure (sender,
++ SOUP_WEBSOCKET_CONNECTION_SERVER,
++ NULL, &error));
++ g_assert_no_error (error);
++
++ receiver = g_object_new (SOUP_TYPE_WEBSOCKET_EXTENSION_DEFLATE, NULL);
++ g_assert_true (soup_websocket_extension_configure (receiver,
++ SOUP_WEBSOCKET_CONNECTION_CLIENT,
++ NULL, &error));
++ g_assert_no_error (error);
++
++ compressed = g_bytes_new_take (g_malloc0 (config->output_size), config->output_size);
++ compressed = soup_websocket_extension_process_outgoing_message (sender, &header,
++ compressed, &error);
++ g_assert_no_error (error);
++ g_assert_nonnull (compressed);
++ g_assert_true (header & 0x40);
++ g_assert_cmpuint (g_bytes_get_size (compressed), <, config->output_size);
++
++ output = soup_websocket_extension_process_incoming_message_with_limit (receiver,
++ &header,
++ compressed,
++ config->max_output_size,
++ &error);
++ if (config->expect_too_big) {
++ g_assert_null (output);
++ g_assert_error (error, SOUP_WEBSOCKET_ERROR, SOUP_WEBSOCKET_CLOSE_TOO_BIG);
++ g_clear_error (&error);
++ } else {
++ g_assert_no_error (error);
++ g_assert_nonnull (output);
++ g_assert_cmpuint (g_bytes_get_size (output), ==, config->output_size);
++ g_bytes_unref (output);
++ }
++
++ g_object_unref (receiver);
++ g_object_unref (sender);
++}
++
++static const DeflateOutputLimitTest deflate_output_below_limit = {
++ 4095, 4096, FALSE
++};
++static const DeflateOutputLimitTest deflate_output_at_limit = {
++ 4096, 4096, FALSE
++};
++static const DeflateOutputLimitTest deflate_output_over_limit = {
++ 4097, 4096, TRUE
++};
++
++typedef struct {
++ gboolean server_receives;
++ gboolean configure_limit;
++} DeflateMessageLimitTest;
++
++static void
++test_deflate_exceeds_message_limit (Test *test,
++ gconstpointer data)
++{
++ const DeflateMessageLimitTest *config = data;
++ SoupWebsocketConnection *receiver;
++ SoupWebsocketConnection *sender;
++ GBytes *received = NULL;
++ GError *error = NULL;
++ guint8 *message;
++
++ if (config->server_receives) {
++ receiver = test->server;
++ sender = test->client;
++ } else {
++ receiver = test->client;
++ sender = test->server;
++ }
++
++ if (config->configure_limit)
++ soup_websocket_connection_set_max_total_message_size (receiver, 128 * 1024);
++
++ g_assert_cmpuint (soup_websocket_connection_get_max_total_message_size (receiver),
++ ==, 128 * 1024);
++
++ g_signal_connect (receiver, "error", G_CALLBACK (on_error_copy_once), &error);
++ g_signal_connect (receiver, "message", G_CALLBACK (on_binary_message), &received);
++
++ message = g_malloc0 (128 * 1024 + 1);
++ soup_websocket_connection_send_binary (sender, message, 128 * 1024 + 1);
++ g_free (message);
++
++ WAIT_UNTIL (error != NULL || received != NULL);
++ g_assert_null (received);
++ g_assert_error (error, SOUP_WEBSOCKET_ERROR, SOUP_WEBSOCKET_CLOSE_TOO_BIG);
++ g_clear_error (&error);
++
++ WAIT_UNTIL (soup_websocket_connection_get_state (sender) == SOUP_WEBSOCKET_STATE_CLOSED);
++ g_assert_null (received);
++ g_assert_cmpuint (soup_websocket_connection_get_close_code (sender),
++ ==, SOUP_WEBSOCKET_CLOSE_TOO_BIG);
++}
++
++static void
++test_deflate_default_unlimited_message_size (Test *test,
++ gconstpointer data)
++{
++ const DeflateMessageLimitTest *config = data;
++ SoupWebsocketConnection *receiver;
++ SoupWebsocketConnection *sender;
++ GBytes *received = NULL;
++ guint8 *message;
++
++ if (config->server_receives) {
++ receiver = test->server;
++ sender = test->client;
++ } else {
++ receiver = test->client;
++ sender = test->server;
++ }
++
++ g_assert_false (config->configure_limit);
++ g_assert_cmpuint (soup_websocket_connection_get_max_total_message_size (receiver), ==, 0);
++ g_signal_connect (receiver, "message", G_CALLBACK (on_binary_message), &received);
++
++ message = g_malloc0 (128 * 1024 + 1);
++ soup_websocket_connection_send_binary (sender, message, 128 * 1024 + 1);
++ g_free (message);
++
++ WAIT_UNTIL (received != NULL);
++ g_assert_cmpuint (g_bytes_get_size (received), ==, 128 * 1024 + 1);
++ g_bytes_unref (received);
++}
++
++static const DeflateMessageLimitTest deflate_server_receives_configured = { TRUE, TRUE };
++static const DeflateMessageLimitTest deflate_client_receives_configured = { FALSE, TRUE };
++static const DeflateMessageLimitTest deflate_server_receives_default = { TRUE, FALSE };
++static const DeflateMessageLimitTest deflate_client_receives_default = { FALSE, FALSE };
++
+ static void
+ do_deflate (z_stream *zstream,
+ const char *str,
+@@ -1771,6 +1997,31 @@ test_receive_fragmented (Test *test,
+ WAIT_UNTIL (soup_websocket_connection_get_state (test->client) == SOUP_WEBSOCKET_STATE_CLOSED);
+ }
+
++static void
++test_deflate_receive_fragmented_too_big (Test *test,
++ gconstpointer data)
++{
++ GThread *thread;
++ GBytes *received = NULL;
++ GError *error = NULL;
++
++ soup_websocket_connection_set_max_total_message_size (test->client, 12);
++ g_signal_connect (test->client, "error", G_CALLBACK (on_error_copy_once), &error);
++ g_signal_connect (test->client, "message", G_CALLBACK (on_text_message), &received);
++
++ thread = g_thread_new ("deflate-fragment-too-big-thread",
++ send_compressed_fragments_server_thread,
++ test);
++
++ WAIT_UNTIL (error != NULL || received != NULL);
++ g_assert_null (received);
++ g_assert_error (error, SOUP_WEBSOCKET_ERROR, SOUP_WEBSOCKET_CLOSE_TOO_BIG);
++ g_clear_error (&error);
++
++ g_thread_join (thread);
++ WAIT_UNTIL (soup_websocket_connection_get_state (test->client) == SOUP_WEBSOCKET_STATE_CLOSED);
++}
++
+ typedef struct {
+ Test *test;
+ const char *header;
+@@ -2798,6 +3049,18 @@ main (int argc,
+ test_deflate_negotiate_direct,
+ NULL);
+
++ g_test_add_func ("/websocket/extension/incoming-limit-legacy-fallback",
++ test_websocket_extension_limit_fallback);
++ g_test_add_data_func ("/websocket/deflate/output-limit/below",
++ &deflate_output_below_limit,
++ test_deflate_output_limit);
++ g_test_add_data_func ("/websocket/deflate/output-limit/exact",
++ &deflate_output_at_limit,
++ test_deflate_output_limit);
++ g_test_add_data_func ("/websocket/deflate/output-limit/over",
++ &deflate_output_over_limit,
++ test_deflate_output_limit);
++
+ g_test_add ("/websocket/direct/deflate-disabled-in-message", Test, NULL, NULL,
+ test_deflate_disabled_in_message_direct,
+ NULL);
+@@ -2823,6 +3086,37 @@ main (int argc,
+ test_send_server_to_client,
+ teardown_soup_connection);
+
++ g_test_add ("/websocket/direct/deflate-configured-limit/client-to-server",
++ Test, &deflate_server_receives_configured,
++ setup_direct_connection_with_extensions,
++ test_deflate_exceeds_message_limit,
++ teardown_direct_connection);
++ g_test_add ("/websocket/direct/deflate-configured-limit/server-to-client",
++ Test, &deflate_client_receives_configured,
++ setup_direct_connection_with_extensions,
++ test_deflate_exceeds_message_limit,
++ teardown_direct_connection);
++ g_test_add ("/websocket/soup/deflate-default-server-limit/client-to-server",
++ Test, &deflate_server_receives_default,
++ setup_soup_connection_with_extensions,
++ test_deflate_exceeds_message_limit,
++ teardown_soup_connection);
++ g_test_add ("/websocket/soup/deflate-configured-client-limit/server-to-client",
++ Test, &deflate_client_receives_configured,
++ setup_soup_connection_with_extensions,
++ test_deflate_exceeds_message_limit,
++ teardown_soup_connection);
++ g_test_add ("/websocket/direct/deflate-default-unlimited/client-to-server",
++ Test, &deflate_server_receives_default,
++ setup_direct_connection_with_extensions,
++ test_deflate_default_unlimited_message_size,
++ teardown_direct_connection);
++ g_test_add ("/websocket/direct/deflate-default-unlimited/server-to-client",
++ Test, &deflate_client_receives_default,
++ setup_direct_connection_with_extensions,
++ test_deflate_default_unlimited_message_size,
++ teardown_direct_connection);
++
+ g_test_add ("/websocket/direct/deflate-send-big-packets", Test, NULL,
+ setup_direct_connection_with_extensions,
+ test_send_big_packets_direct,
+@@ -2845,6 +3139,10 @@ main (int argc,
+ setup_half_direct_connection_with_extensions,
+ test_receive_fragmented,
+ teardown_direct_connection);
++ g_test_add ("/websocket/direct/deflate-receive-fragmented-too-big", Test, NULL,
++ setup_half_direct_connection_with_extensions,
++ test_deflate_receive_fragmented_too_big,
++ teardown_direct_connection);
+ g_test_add ("/websocket/direct/deflate-receive-fragmented-error", Test, NULL,
+ setup_half_direct_connection_with_extensions,
+ test_deflate_receive_fragmented_error,
+--
+GitLab
+
diff --git a/CVE-2026-15711.patch b/CVE-2026-15711.patch
new file mode 100644
index 0000000..22ab57b
--- /dev/null
+++ b/CVE-2026-15711.patch
@@ -0,0 +1,172 @@
+From 60aa1ce2bdc7bb5da33be9062f50bcec7db67fca Mon Sep 17 00:00:00 2001
+From: Zayd Rajab <zaydr@amazon.com>
+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 14dcd630..ffd713ef 100644
+--- a/libsoup/websocket/soup-websocket-connection.c
++++ b/libsoup/websocket/soup-websocket-connection.c
+@@ -1082,6 +1082,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 b8f71300..db088a55 100644
+--- a/tests/websocket-test.c
++++ b/tests/websocket-test.c
+@@ -835,6 +835,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";
+@@ -2574,6 +2666,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
+
diff --git a/CVE-2026-15712.patch b/CVE-2026-15712.patch
new file mode 100644
index 0000000..5616dec
--- /dev/null
+++ b/CVE-2026-15712.patch
@@ -0,0 +1,38 @@
+From 3a6fb56a0cba42d11f5fd1db6dedcc7c2e92757b Mon Sep 17 00:00:00 2001
+From: Mike Gorse <mgorse@suse.com>
+Date: Fri, 4 Sep 2026 14:05:04 -0500
+Subject: [PATCH] http2: Don't assume that GOAWAY opaque data is NUL-terminated
+
+nghttp2 allocates frame->goaway.opaque_data with exactly opaque_data_len
+bytes and no terminator, so passing it to a "%s" format in h2_debug()
+reads past the end of the buffer when debug logging is enabled.
+
+Use "%.*s" with opaque_data_len as the precision so the read is bounded
+without allocating a copy.
+
+Closes #540
+---
+ libsoup/http2/soup-client-message-io-http2.c | 5 +++--
+ 1 file changed, 3 insertions(+), 2 deletions(-)
+
+diff --git a/libsoup/http2/soup-client-message-io-http2.c b/libsoup/http2/soup-client-message-io-http2.c
+index ab52003d..b6742924 100644
+--- a/libsoup/http2/soup-client-message-io-http2.c
++++ b/libsoup/http2/soup-client-message-io-http2.c
+@@ -813,10 +813,11 @@ on_frame_recv_callback (nghttp2_session *session,
+
+ switch (frame->hd.type) {
+ case NGHTTP2_GOAWAY:
+- h2_debug (io, NULL, "[RECV] GOAWAY: error=%s, last_stream_id=%d %s",
++ h2_debug (io, NULL, "[RECV] GOAWAY: error=%s, last_stream_id=%d %.*s",
+ nghttp2_http2_strerror (frame->goaway.error_code),
+ frame->goaway.last_stream_id,
+- frame->goaway.opaque_data ? (char *)frame->goaway.opaque_data : "");
++ (int)frame->goaway.opaque_data_len,
++ frame->goaway.opaque_data ? (const char *)frame->goaway.opaque_data : "");
+ handle_goaway (io, frame->goaway.error_code, frame->goaway.last_stream_id);
+ io->is_shutdown = TRUE;
+ soup_client_message_io_http2_terminate_session (io);
+--
+GitLab
+
diff --git a/CVE-2026-15713.patch b/CVE-2026-15713.patch
new file mode 100644
index 0000000..ef84ebb
--- /dev/null
+++ b/CVE-2026-15713.patch
@@ -0,0 +1,148 @@
+From 24fb645fa949ece7d7e10363b77cf2d5fa2c2469 Mon Sep 17 00:00:00 2001
+From: Milan Crha <mcrha@redhat.com>
+Date: Fri, 4 Sep 2026 10:34:22 +0200
+Subject: [PATCH] cache: Reject caching authenticated responses in shared
+ caches
+
+RFC 7234 Section 3.2 forbids to save to a shared cache a response to
+a request containing an Authorization header unless the response
+explicitly allows it with public, must-revalidate or s-maxage.
+
+This is CVE-2026-15713.
+
+Closes https://gitlab.gnome.org/GNOME/libsoup/-/work_items/541
+---
+ libsoup/cache/soup-cache.c | 15 +++++++++++
+ tests/cache-test.c | 55 ++++++++++++++++++++++++++++++++++++++
+ 2 files changed, 70 insertions(+)
+
+diff --git a/libsoup/cache/soup-cache.c b/libsoup/cache/soup-cache.c
+index 47e1ee70..5225382d 100644
+--- a/libsoup/cache/soup-cache.c
++++ b/libsoup/cache/soup-cache.c
+@@ -174,6 +174,7 @@ get_cacheability (SoupCache *cache, SoupMessage *msg)
+ SoupCacheability cacheability;
+ const char *cache_control, *content_type;
+ gboolean has_max_age = FALSE;
++ gboolean permits_authorized_shared_caching = FALSE;
+
+ /* 1. The request method must be cacheable */
+ if (soup_message_get_method (msg) == SOUP_METHOD_GET)
+@@ -201,6 +202,11 @@ get_cacheability (SoupCache *cache, SoupMessage *msg)
+ soup_header_free_param_list (hash);
+ return SOUP_CACHE_UNCACHEABLE;
+ }
++
++ permits_authorized_shared_caching =
++ g_hash_table_lookup_extended (hash, "public", NULL, NULL) ||
++ g_hash_table_lookup_extended (hash, "must-revalidate", NULL, NULL) ||
++ g_hash_table_lookup_extended (hash, "s-maxage", NULL, NULL);
+ }
+
+ /* 2. The 'no-store' cache directive does not appear in the
+@@ -225,6 +231,15 @@ get_cacheability (SoupCache *cache, SoupMessage *msg)
+ soup_header_free_param_list (hash);
+ }
+
++ /* RFC 7234 Section 3.2: a shared cache MUST NOT store a response to
++ * a request containing an Authorization header unless the response
++ * explicitly allows it with public, must-revalidate or s-maxage.
++ */
++ if (priv->cache_type == SOUP_CACHE_SHARED &&
++ !permits_authorized_shared_caching &&
++ soup_message_headers_get_one_common (soup_message_get_request_headers (msg), SOUP_HEADER_AUTHORIZATION))
++ return SOUP_CACHE_UNCACHEABLE;
++
+ /* Section 13.9 */
+ if ((g_uri_get_query (soup_message_get_uri (msg))) &&
+ !soup_message_headers_get_one_common (soup_message_get_response_headers (msg), SOUP_HEADER_EXPIRES) &&
+diff --git a/tests/cache-test.c b/tests/cache-test.c
+index ef2ebeb3..dff6bd35 100644
+--- a/tests/cache-test.c
++++ b/tests/cache-test.c
+@@ -95,6 +95,7 @@ server_callback (SoupServer *server,
+ if (status == SOUP_STATUS_OK) {
+ GChecksum *sum;
+ const char *body;
++ const char *authorization;
+
+ sum = g_checksum_new (G_CHECKSUM_SHA256);
+ g_checksum_update (sum, (guchar *)path, strlen (path));
+@@ -102,6 +103,9 @@ server_callback (SoupServer *server,
+ g_checksum_update (sum, (guchar *)last_modified, strlen (last_modified));
+ if (etag)
+ g_checksum_update (sum, (guchar *)etag, strlen (etag));
++ authorization = soup_message_headers_get_one (request_headers, "Authorization");
++ if (authorization)
++ g_checksum_update (sum, (guchar *)authorization, strlen (authorization));
+ body = g_checksum_get_string (sum);
+ soup_server_message_set_response (msg, "text/plain",
+ SOUP_MEMORY_COPY,
+@@ -736,6 +740,56 @@ do_leaks_test (gconstpointer data)
+ g_free (cache_dir);
+ }
+
++static void
++do_shared_cache_authorization_test (gconstpointer data)
++{
++ GUri *base_uri = (GUri *)data;
++ SoupSession *attacker_session;
++ SoupSession *victim_session;
++ SoupCache *attacker_cache;
++ SoupCache *victim_cache;
++ char *cache_dir;
++ char *attacker_body;
++ char *victim_body;
++
++ cache_dir = g_dir_make_tmp ("cache-test-XXXXXX", NULL);
++ debug_printf (2, " Caching to %s\n", cache_dir);
++
++ attacker_cache = soup_cache_new (cache_dir, SOUP_CACHE_SHARED);
++ attacker_session = soup_test_session_new (NULL);
++ soup_session_add_feature (attacker_session, SOUP_SESSION_FEATURE (attacker_cache));
++
++ attacker_body = do_request (attacker_session, base_uri, "GET", "/authz-shared-cache", NULL,
++ "Authorization", "Bearer ATTACKER_SECRET",
++ "Test-Set-Cache-Control", "max-age=600",
++ NULL);
++ g_assert_true (last_request_hit_network);
++
++ soup_cache_dump (attacker_cache);
++ soup_test_session_abort_unref (attacker_session);
++ g_object_unref (attacker_cache);
++
++ victim_cache = soup_cache_new (cache_dir, SOUP_CACHE_SHARED);
++ soup_cache_load (victim_cache);
++ victim_session = soup_test_session_new (NULL);
++ soup_session_add_feature (victim_session, SOUP_SESSION_FEATURE (victim_cache));
++
++ victim_body = do_request (victim_session, base_uri, "GET", "/authz-shared-cache", NULL,
++ "Authorization", "Bearer VICTIM_SECRET",
++ "Test-Set-Cache-Control", "max-age=600",
++ NULL);
++
++ g_assert_true (last_request_hit_network);
++ g_assert_cmpstr (attacker_body, !=, victim_body);
++
++ g_free (attacker_body);
++ g_free (victim_body);
++
++ soup_test_session_abort_unref (victim_session);
++ g_object_unref (victim_cache);
++ g_free (cache_dir);
++}
++
+ static void
+ do_metrics_test (gconstpointer data)
+ {
+@@ -1108,6 +1162,7 @@ main (int argc, char **argv)
+ g_test_add_data_func ("/cache/cancellation", base_uri, do_cancel_test);
+ g_test_add_data_func ("/cache/refcounting", base_uri, do_refcounting_test);
+ g_test_add_data_func ("/cache/headers", base_uri, do_headers_test);
++ g_test_add_data_func ("/cache/shared-cache-authorization", base_uri, do_shared_cache_authorization_test);
+ g_test_add_data_func ("/cache/leaks", base_uri, do_leaks_test);
+ g_test_add_data_func ("/cache/metrics", base_uri, do_metrics_test);
+ g_test_add_data_func ("/cache/threads", base_uri, do_threads_test);
+--
+GitLab
+
diff --git a/CVE-2026-15714.patch b/CVE-2026-15714.patch
new file mode 100644
index 0000000..451f767
--- /dev/null
+++ b/CVE-2026-15714.patch
@@ -0,0 +1,121 @@
+From 79a52cadc490360e249cc2b23038d532b44dbf23 Mon Sep 17 00:00:00 2001
+From: Mike Gorse <mgorse@suse.com>
+Date: Fri, 4 Sep 2026 14:10:46 -0500
+Subject: [PATCH] multipart: Fix out-of-bounds read in case of an excessively
+ long boundary
+
+soup_multipart_input_stream_read_headers() compared meta_buf against the
+boundary using the boundary length without checking that meta_buf held
+that many bytes, so a server sending a boundary longer than the read
+buffer could make strncmp() read past the end of the allocation. It also
+computed read_buf + nread - 4 before verifying nread was large enough,
+and asserted rather than failing when a line did not end in a newline.
+
+Only compare once enough bytes have been read, index read_buf after the
+length checks, and treat an unterminated line as end of parsing.
+
+Closes !546
+---
+ libsoup/soup-multipart-input-stream.c | 12 ++++-----
+ tests/multipart-test.c | 37 +++++++++++++++++++++++++++
+ 2 files changed, 43 insertions(+), 6 deletions(-)
+
+diff --git a/libsoup/soup-multipart-input-stream.c b/libsoup/soup-multipart-input-stream.c
+index b7f1ed72..22cce102 100644
+--- a/libsoup/soup-multipart-input-stream.c
++++ b/libsoup/soup-multipart-input-stream.c
+@@ -361,7 +361,6 @@ soup_multipart_input_stream_read_headers (SoupMultipartInputStream *multipart,
+ {
+ SoupMultipartInputStreamPrivate *priv = soup_multipart_input_stream_get_instance_private (multipart);
+ guchar read_buf[RESPONSE_BLOCK_SIZE];
+- guchar *buf;
+ gboolean got_boundary = FALSE;
+ gboolean got_lf = FALSE;
+ gssize nread = 0;
+@@ -383,22 +382,23 @@ soup_multipart_input_stream_read_headers (SoupMultipartInputStream *multipart,
+ * may get the multipart end indicator without getting a new line.
+ */
+ if (!got_boundary &&
++ priv->meta_buf->len >= priv->boundary_size &&
+ !strncmp ((char *)priv->meta_buf->data,
+ priv->boundary,
+ priv->boundary_size)) {
+ got_boundary = TRUE;
+
+ /* Now check for possible multipart termination. */
+- buf = &read_buf[nread - 4];
+- if ((nread >= 4 && !memcmp (buf, "--\r\n", 4)) ||
+- (nread >= 3 && !memcmp (buf + 1, "--\n", 3)) ||
+- (nread >= 3 && !memcmp (buf + 2, "--", 2))) {
++ if ((nread >= 4 && !memcmp (read_buf + nread - 4, "--\r\n", 4)) ||
++ (nread >= 3 && !memcmp (read_buf + nread - 3, "--\n", 3)) ||
++ (nread >= 3 && !memcmp (read_buf + nread - 2, "--", 2))) {
+ g_byte_array_set_size (priv->meta_buf, 0);
+ return FALSE;
+ }
+ }
+
+- g_return_val_if_fail (got_lf, FALSE);
++ if (!got_lf)
++ return FALSE;
+
+ /* Discard pre-boundary lines. */
+ if (!got_boundary) {
+diff --git a/tests/multipart-test.c b/tests/multipart-test.c
+index b1bf5bb8..a598a1c8 100644
+--- a/tests/multipart-test.c
++++ b/tests/multipart-test.c
+@@ -596,6 +596,42 @@ test_multipart_bounds_bad_3 (void)
+ g_object_unref (msg);
+ }
+
++static void
++test_multipart_bounds_bad_4 (void)
++{
++ SoupMessage *msg;
++ SoupMessageHeaders *headers;
++ GInputStream *in;
++ GInputStream *next_part;
++ SoupMultipartInputStream *multipart;
++ GError *error = NULL;
++ gchar *boundary;
++ gchar *content_type;
++ gchar *raw_data;
++
++ boundary = g_new (gchar, 20001);
++ memset (boundary, 'a', 20000);
++ boundary[20000] = '\0';
++
++ msg = soup_message_new(SOUP_METHOD_POST, "http://foo/upload");
++ headers = soup_message_get_response_headers (msg);
++ content_type = g_strconcat ("multipart/form-data; boundary=\"", boundary, "\"", NULL);
++ soup_message_headers_replace (headers, "Content-Type", content_type);
++
++ raw_data = g_strconcat ("--", boundary, "\r\nContent-Type: text-plain\r\n\r\nX\r\n", NULL);
++ in = g_memory_input_stream_new_from_data (raw_data, strlen (raw_data), NULL);
++ multipart = soup_multipart_input_stream_new (msg, in);
++ g_object_unref (in);
++ next_part = soup_multipart_input_stream_next_part (multipart, NULL, &error);
++ g_assert_no_error (error);
++ g_assert_null (next_part);
++ g_object_unref (multipart);
++ g_object_unref (msg);
++ g_free (raw_data);
++ g_free (content_type);
++ g_free (boundary);
++}
++
+ static void
+ test_multipart_too_large (void)
+ {
+@@ -666,6 +702,7 @@ main (int argc, char **argv)
+ g_test_add_func ("/multipart/bounds-bad", test_multipart_bounds_bad);
+ g_test_add_func ("/multipart/bounds-bad-2", test_multipart_bounds_bad_2);
+ g_test_add_func ("/multipart/bounds-bad-3", test_multipart_bounds_bad_3);
++ g_test_add_func ("/multipart/bounds-bad-4", test_multipart_bounds_bad_4);
+ g_test_add_func ("/multipart/too-large", test_multipart_too_large);
+
+ ret = g_test_run ();
+--
+GitLab
+
diff --git a/CVE-2026-77680.patch b/CVE-2026-77680.patch
new file mode 100644
index 0000000..2e142b0
--- /dev/null
+++ b/CVE-2026-77680.patch
@@ -0,0 +1,681 @@
+From e82c13ba03defcee10f981ac964f4d570b21a251 Mon Sep 17 00:00:00 2001
+From: Patrick Griffis <pgriffis@igalia.com>
+Date: Wed, 12 Aug 2026 14:21:52 -0500
+Subject: [PATCH] message-headers: Fix Range parsing overflows and coalescing
+ cost
+
+A suffix range longer than the body drove the range start negative, which
+then reached a g_assert() and aborted the process. Clamp it to select the
+whole body instead, as RFC 9110 section 14.1.2 requires. Range starts and
+ends which overflow the goffset they are parsed into are now rejected and
+clamped respectively, rather than wrapping negative and reaching the same
+assertion.
+
+Ranges are no longer coalesced by removing each merged element in turn,
+which was quadratic in their number, and a Range header listing more than
+MAX_RANGES ranges is now answered with 416 instead of being served; RFC
+9110 section 15.5.17 names an excessive number of ranges as a reason for
+that status.
+
+Finally, sort_ranges() returned a goffset difference truncated to int, so
+it reported the wrong order for ranges more than G_MAXINT apart, and the
+merge below it then dropped ranges from responses over 2GB.
+
+Fixes #516
+Fixes #519
+Fixes #535
+Fixes #538
+Fixes #544
+Fixes #547
+Fixes #548
+---
+ libsoup/soup-message-headers-private.h | 2 +
+ libsoup/soup-message-headers.c | 89 +++++--
+ tests/range-test.c | 347 +++++++++++++++++++++++++
+ tests/server-mem-limit-test.c | 61 +++--
+ 4 files changed, 464 insertions(+), 35 deletions(-)
+
+diff --git a/libsoup/soup-message-headers-private.h b/libsoup/soup-message-headers-private.h
+index 708afe98..2e96af34 100644
+--- a/libsoup/soup-message-headers-private.h
++++ b/libsoup/soup-message-headers-private.h
+@@ -12,6 +12,8 @@ G_BEGIN_DECLS
+
+ #define MAX_HEADERS_BUFFER_SIZE 256 * 1024 /* 256K */
+
++#define MAX_RANGES 200
++
+ typedef enum {
+ SOUP_HEADER_VALUE_UNTRUSTED,
+ SOUP_HEADER_VALUE_TRUSTED
+diff --git a/libsoup/soup-message-headers.c b/libsoup/soup-message-headers.c
+index b2ec036c..7e2e6583 100644
+--- a/libsoup/soup-message-headers.c
++++ b/libsoup/soup-message-headers.c
+@@ -1226,7 +1226,12 @@ sort_ranges (gconstpointer a, gconstpointer b)
+ SoupRange *ra = (SoupRange *)a;
+ SoupRange *rb = (SoupRange *)b;
+
+- return ra->start - rb->start;
++ if (ra->start < rb->start)
++ return -1;
++ else if (ra->start > rb->start)
++ return 1;
++ else
++ return 0;
+ }
+
+ /* like soup_message_headers_get_ranges(), except it returns:
+@@ -1270,6 +1275,17 @@ soup_message_headers_get_ranges_internal (SoupMessageHeaders *hdrs,
+ if (!range_list)
+ return SOUP_STATUS_OK; /* invalid list */
+
++ /* Reject the header outright if it asks for more ranges than we are
++ * willing to serve, rather than answering with the whole body: a client
++ * asking for this many ranges wants to be told so, and RFC 9110 §14.2
++ * allows rejecting such a header for exactly this reason.
++ */
++ if (g_slist_length (range_list) > MAX_RANGES) {
++ soup_header_free_list (range_list);
++ return check_satisfiable ? SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE
++ : SOUP_STATUS_OK;
++ }
++
+ /* Loop through the ranges and modify the status accordingly. Default to
+ * status 200 (OK, ignoring the ranges). Switch to status 206 (Partial
+ * Content) if there is at least one partially valid range. Switch to
+@@ -1281,15 +1297,48 @@ soup_message_headers_get_ranges_internal (SoupMessageHeaders *hdrs,
+
+ spec = r->data;
+ if (*spec == '-') {
+- cur.start = g_ascii_strtoll (spec, &end, 10) + total_length;
++ gint64 suffix_length;
++
++ errno = 0;
++ suffix_length = g_ascii_strtoll (spec, &end, 10);
++
++ /* A suffix range asks for the last -suffix_length bytes
++ * of the body. If the body is shorter than that then the
++ * whole body is used, per RFC 9110 §14.1.2; without
++ * clamping, the start would go negative and reach the
++ * assertion below.
++ */
++ if (errno == ERANGE || suffix_length <= -total_length)
++ cur.start = 0;
++ else
++ cur.start = total_length + suffix_length;
++
+ cur.end = total_length - 1;
+ } else {
+- cur.start = g_ascii_strtoull (spec, &end, 10);
++ guint64 value;
++
++ errno = 0;
++ value = g_ascii_strtoull (spec, &end, 10);
++ if (errno == ERANGE || value > G_MAXINT64) {
++ is_all_valid = FALSE;
++ continue;
++ }
++ cur.start = (goffset) value;
++
+ if (*end == '-')
+ end++;
+- if (*end)
+- cur.end = g_ascii_strtoull (end, &end, 10);
+- else
++ if (*end) {
++ errno = 0;
++ value = g_ascii_strtoull (end, &end, 10);
++
++ /* An end this large is clamped to the end of the
++ * body below, like any other end past it.
++ */
++ if (errno == ERANGE || value > G_MAXINT64)
++ cur.end = G_MAXINT64;
++ else
++ cur.end = (goffset) value;
++ } else
+ cur.end = total_length - 1;
+ }
+
+@@ -1330,19 +1379,24 @@ soup_message_headers_get_ranges_internal (SoupMessageHeaders *hdrs,
+ }
+
+ if (total_length) {
+- guint i;
++ SoupRange *data;
++ guint i, last = 0;
+
+ g_array_sort (array, sort_ranges);
+- for (i = 1; i < array->len; i++) {
+- SoupRange *cur = &((SoupRange *)array->data)[i];
+- SoupRange *prev = &((SoupRange *)array->data)[i - 1];
+
+- if (cur->start <= prev->end) {
+- prev->end = MAX (prev->end, cur->end);
+- g_array_remove_index (array, i);
+- i--;
+- }
++ /* Merge overlapping ranges into the run being built at @last.
++ * Removing the merged elements one at a time instead made this
++ * quadratic in the number of ranges.
++ */
++ data = (SoupRange *)array->data;
++ for (i = 1; i < array->len; i++) {
++ if (data[i].start <= data[last].end)
++ data[last].end = MAX (data[last].end, data[i].end);
++ else
++ data[++last] = data[i];
+ }
++
++ g_array_set_size (array, last + 1);
+ }
+
+ *ranges = (SoupRange *)array->data;
+@@ -1375,6 +1429,11 @@ soup_message_headers_get_ranges_internal (SoupMessageHeaders *hdrs,
+ * Beware that even if given a @total_length, this function does not
+ * check that the ranges are satisfiable.
+ *
++ * A Range header requesting more than 200 ranges is rejected, since serving
++ * that many ranges costs far more than the request asking for them.
++ * [class@Server] answers such a request with
++ * %SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE.
++ *
+ * [class@Server] has built-in handling for range requests. If your
+ * server handler returns a %SOUP_STATUS_OK response containing the
+ * complete response body (rather than pausing the message and
+diff --git a/tests/range-test.c b/tests/range-test.c
+index cfd5f613..9e4ed2c9 100644
+--- a/tests/range-test.c
++++ b/tests/range-test.c
+@@ -3,6 +3,8 @@
+ #include "config.h"
+
+ #include "test-utils.h"
++#include "soup-message-headers-private.h"
++#include "soup-misc.h"
+
+ GBytes *full_response;
+ int total_length;
+@@ -161,6 +163,21 @@ request_single_range_by_string (SoupSession *session, const char *uri,
+ g_object_unref (msg);
+ }
+
++/* Like request_single_range_by_string(), but able to check the ranges of a
++ * successful 206 as well. */
++static void
++request_single_range_by_string_full (SoupSession *session, const char *uri,
++ const char *range, SoupStatus expected_status,
++ int expected_start, int expected_end)
++{
++ SoupMessage *msg;
++
++ msg = soup_message_new ("GET", uri);
++ soup_message_headers_replace (soup_message_get_request_headers (msg), "Range", range);
++
++ do_single_range (session, msg, 0, 0, expected_status, expected_start, expected_end);
++}
++
+ static void
+ do_multi_range (SoupSession *session, SoupMessage *msg,
+ int expected_return_ranges)
+@@ -445,6 +462,290 @@ do_range_test (SoupSession *session, const char *uri,
+ SOUP_STATUS_OK);
+ }
+
++/* Tests for the Range parser itself. Unlike the tests above, these don't need
++ * a server, so they can use total lengths which would be impractical to
++ * actually serve, and they can check the exact status which the server would
++ * use rather than only the ones a client can distinguish.
++ */
++typedef struct {
++ const char *description;
++ const char *bugref;
++ const char *range;
++ goffset total_length;
++ guint expected_status;
++ int expected_n_ranges;
++ SoupRange expected_ranges[3];
++} RangeParsingTest;
++
++static const RangeParsingTest range_parsing_tests[] = {
++ /* Valid ranges against a ten byte body, as a baseline. */
++ { "simple range", NULL,
++ "bytes=0-4", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 4 } } },
++ { "whole body", NULL,
++ "bytes=0-9", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 9 } } },
++ { "open ended range", NULL,
++ "bytes=5-", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 5, 9 } } },
++ { "final byte", NULL,
++ "bytes=9-", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 9, 9 } } },
++ { "end past the body is clamped", NULL,
++ "bytes=1-100", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 1, 9 } } },
++ { "suffix range", NULL,
++ "bytes=-5", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 5, 9 } } },
++ { "single byte suffix range", NULL,
++ "bytes=-1", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 9, 9 } } },
++ { "whitespace around the ranges", NULL,
++ "bytes \t = \t 0-4", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 4 } } },
++
++ /* Unsatisfiable and invalid ranges. */
++ { "start past the body", NULL,
++ "bytes=10-20", 10, SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE, 0, { } },
++ { "end before start", NULL,
++ "bytes=10-1", 10, SOUP_STATUS_OK, 0, { } },
++ { "zero length suffix range", NULL,
++ "bytes=-0", 10, SOUP_STATUS_OK, 0, { } },
++ { "trailing garbage", NULL,
++ "bytes=0-10 but with weird trailing content", 10, SOUP_STATUS_OK, 0, { } },
++ { "invalid range dash", NULL,
++ "bytes=0a10", 10, SOUP_STATUS_OK, 0, { } },
++ { "unknown range unit", NULL,
++ "horses=0-10", 10, SOUP_STATUS_OK, 0, { } },
++ { "missing equals", NULL,
++ "bytes 0-10", 10, SOUP_STATUS_OK, 0, { } },
++ { "delimiters but no ranges", NULL,
++ "bytes=, ,,\t, ", 10, SOUP_STATUS_OK, 0, { } },
++
++ /* A suffix length at least as long as the body selects the whole body,
++ * per RFC 9110 §14.1.2. These used to drive the range start negative,
++ * which aborted the process at the g_assert() below the parse.
++ */
++ { "suffix range the length of the body", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/516",
++ "bytes=-10", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 9 } } },
++ { "suffix range one longer than the body", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/516",
++ "bytes=-11", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 9 } } },
++ { "suffix range much longer than the body", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/548",
++ "bytes=-999999", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 9 } } },
++ { "suffix range of G_MININT", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/547",
++ "bytes=-2147483648", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 9 } } },
++ { "suffix range larger than a guint32", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/547",
++ "bytes=-4294967296", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 9 } } },
++ { "suffix range of G_MAXINT64", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/547",
++ "bytes=-9223372036854775807", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 9 } } },
++ { "suffix range of G_MININT64", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/547",
++ "bytes=-9223372036854775808", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 9 } } },
++ { "suffix range overflowing gint64", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/535",
++ "bytes=-99999999999999999999", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 9 } } },
++ { "oversized suffix range merged with a valid range", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/548",
++ "bytes=-999999,4-5", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 9 } } },
++
++ /* Range starts and ends which overflow the signed goffset they are
++ * parsed into. An overflowing start is treated like any other start
++ * beyond the end of the body, and an overflowing end is clamped like
++ * any other end beyond the end of the body.
++ */
++ { "start overflowing gint64", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/535",
++ "bytes=9888888888888019900-", 10, SOUP_STATUS_OK, 0, { } },
++ { "start overflowing gint64 with no dash", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/535",
++ "bytes=9888888888888019900", 10, SOUP_STATUS_OK, 0, { } },
++ { "start of G_MAXINT64 + 1", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/535",
++ "bytes=9223372036854775808-", 10, SOUP_STATUS_OK, 0, { } },
++ { "start overflowing guint64", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/535",
++ "bytes=18446744073709551616-", 10, SOUP_STATUS_OK, 0, { } },
++ { "start and end overflowing gint64", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/535",
++ "bytes=9888888888888019900-9888888888888019901", 10, SOUP_STATUS_OK, 0, { } },
++ { "end overflowing gint64", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/535",
++ "bytes=0-9888888888888019900", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 9 } } },
++ { "end overflowing guint64", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/535",
++ "bytes=0-18446744073709551616", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 9 } } },
++
++ /* Zero length bodies. soup_message_headers_get_ranges() is public API,
++ * so it can be called with one even though the server never does.
++ */
++ { "range against an empty body", NULL,
++ "bytes=0-9", 0, SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE, 0, { } },
++ { "suffix range against an empty body", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/548",
++ "bytes=-5", 0, SOUP_STATUS_OK, 0, { } },
++
++ /* Merging. */
++ { "overlapping ranges are merged", NULL,
++ "bytes=0-10,5-20", 100, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 20 } } },
++ { "contained ranges are merged", NULL,
++ "bytes=0-20,5-10", 100, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 20 } } },
++ { "touching ranges are not merged", NULL,
++ "bytes=0-4,5-9", 100, SOUP_STATUS_PARTIAL_CONTENT, 2, { { 0, 4 }, { 5, 9 } } },
++ { "ranges are sorted", NULL,
++ "bytes=20-29,0-9", 100, SOUP_STATUS_PARTIAL_CONTENT, 2, { { 0, 9 }, { 20, 29 } } },
++ { "invalid ranges do not prevent valid ones", NULL,
++ "bytes=0-9,50-40,20-29", 100, SOUP_STATUS_PARTIAL_CONTENT, 2, { { 0, 9 }, { 20, 29 } } },
++
++ /* The comparison function used to sort the ranges before merging them
++ * used to truncate a goffset difference to int, which flips its sign
++ * for bodies over 2GB and silently dropped ranges from the response.
++ */
++ { "ranges more than G_MAXINT apart", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/519",
++ "bytes=0-100, 2500000000-2500000100, 4999999000-4999999100", 5000000000,
++ SOUP_STATUS_PARTIAL_CONTENT, 3,
++ { { 0, 100 }, { 2500000000, 2500000100 }, { 4999999000, 4999999100 } } },
++ { "unsorted ranges more than G_MAXINT apart", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/519",
++ "bytes=4999999000-4999999100, 0-100, 2500000000-2500000100", 5000000000,
++ SOUP_STATUS_PARTIAL_CONTENT, 3,
++ { { 0, 100 }, { 2500000000, 2500000100 }, { 4999999000, 4999999100 } } },
++};
++
++static void
++check_parsed_ranges (const char *range,
++ goffset total_length,
++ guint expected_status,
++ int expected_n_ranges,
++ const SoupRange *expected_ranges)
++{
++ SoupMessageHeaders *hdrs;
++ SoupRange *ranges = NULL;
++ int n_ranges = 0;
++ guint status;
++ int i;
++
++ hdrs = soup_message_headers_new (SOUP_MESSAGE_HEADERS_REQUEST);
++ soup_message_headers_replace (hdrs, "Range", range);
++
++ status = soup_message_headers_get_ranges_internal (hdrs, total_length, TRUE,
++ &ranges, &n_ranges);
++
++ g_assert_cmpuint (status, ==, expected_status);
++
++ if (status == SOUP_STATUS_PARTIAL_CONTENT) {
++ g_assert_nonnull (ranges);
++ g_assert_cmpint (n_ranges, ==, expected_n_ranges);
++
++ for (i = 0; i < n_ranges; i++) {
++ debug_printf (2, " [%d]: %" G_GINT64_FORMAT "-%" G_GINT64_FORMAT "\n",
++ i, ranges[i].start, ranges[i].end);
++
++ g_assert_cmpint (ranges[i].start, ==, expected_ranges[i].start);
++ g_assert_cmpint (ranges[i].end, ==, expected_ranges[i].end);
++
++ /* Whatever the input, the parsed ranges must be usable
++ * as offsets into a buffer of total_length bytes.
++ */
++ g_assert_cmpint (ranges[i].start, >=, 0);
++ g_assert_cmpint (ranges[i].end, >=, ranges[i].start);
++ g_assert_cmpint (ranges[i].end, <, total_length);
++ }
++ }
++
++ soup_message_headers_free_ranges (hdrs, ranges);
++ soup_message_headers_unref (hdrs);
++}
++
++static void
++do_range_parsing_test (void)
++{
++ guint i;
++
++ for (i = 0; i < G_N_ELEMENTS (range_parsing_tests); i++) {
++ const RangeParsingTest *test = &range_parsing_tests[i];
++
++ debug_printf (1, "%2u. %s: '%s' against %" G_GOFFSET_FORMAT " bytes\n",
++ i + 1, test->description, test->range, test->total_length);
++
++ if (test->bugref)
++ g_test_message ("Bug reference: %s", test->bugref);
++
++ check_parsed_ranges (test->range, test->total_length,
++ test->expected_status, test->expected_n_ranges,
++ test->expected_ranges);
++ }
++}
++
++/* A single Range header can list far more ranges than are reasonable to serve:
++ * the only limit on the wire is the maximum request header size. */
++static void
++do_range_count_test (void)
++{
++ struct {
++ int n_ranges;
++ gboolean identical;
++ guint expected_status;
++ int expected_n_ranges;
++ } tests[] = {
++ /* Distinct ranges, up to and then past the limit. Going past it
++ * is rejected rather than ignored.
++ */
++ { 100, FALSE, SOUP_STATUS_PARTIAL_CONTENT, 100 },
++ { MAX_RANGES, FALSE, SOUP_STATUS_PARTIAL_CONTENT, MAX_RANGES },
++ { MAX_RANGES + 1, FALSE, SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE, 0 },
++
++ /* Identical ranges, which all merge into one. This is the
++ * shape which used to be quadratic.
++ */
++ { MAX_RANGES, TRUE, SOUP_STATUS_PARTIAL_CONTENT, 1 },
++ { MAX_RANGES + 1, TRUE, SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE, 0 },
++ { 25585, TRUE, SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE, 0 },
++ };
++ guint i;
++ int j;
++
++ for (i = 0; i < G_N_ELEMENTS (tests); i++) {
++ SoupMessageHeaders *hdrs;
++ SoupRange *ranges = NULL;
++ int n_ranges = 0;
++ guint status;
++ GString *range;
++
++ debug_printf (1, "%2u. %d %s ranges\n", i + 1, tests[i].n_ranges,
++ tests[i].identical ? "identical" : "distinct");
++
++ range = g_string_new ("bytes=");
++ for (j = 0; j < tests[i].n_ranges; j++) {
++ int start = tests[i].identical ? 0 : j * 2;
++
++ if (j > 0)
++ g_string_append_c (range, ',');
++ g_string_append_printf (range, "%d-%d", start, start);
++ }
++
++ hdrs = soup_message_headers_new (SOUP_MESSAGE_HEADERS_REQUEST);
++ soup_message_headers_replace (hdrs, "Range", range->str);
++ g_string_free (range, TRUE);
++
++ status = soup_message_headers_get_ranges_internal (hdrs, 1000000, TRUE,
++ &ranges, &n_ranges);
++
++ g_assert_cmpuint (status, ==, tests[i].expected_status);
++ if (status == SOUP_STATUS_PARTIAL_CONTENT)
++ g_assert_cmpint (n_ranges, ==, tests[i].expected_n_ranges);
++
++ soup_message_headers_free_ranges (hdrs, ranges);
++ soup_message_headers_unref (hdrs);
++ }
++
++ /* Callers which don't ask about satisfiability, such as the public
++ * soup_message_headers_get_ranges(), can't be told 416, so an
++ * over-limit header just reports no ranges to them.
++ */
++ {
++ SoupMessageHeaders *hdrs;
++ SoupRange *ranges = NULL;
++ int n_ranges = 0;
++ GString *range;
++
++ range = g_string_new ("bytes=0-0");
++ for (j = 0; j < MAX_RANGES; j++)
++ g_string_append (range, ",0-0");
++
++ hdrs = soup_message_headers_new (SOUP_MESSAGE_HEADERS_REQUEST);
++ soup_message_headers_replace (hdrs, "Range", range->str);
++ g_string_free (range, TRUE);
++
++ g_assert_cmpuint (soup_message_headers_get_ranges_internal (hdrs, 1000000, FALSE,
++ &ranges, &n_ranges),
++ ==, SOUP_STATUS_OK);
++ g_assert_false (soup_message_headers_get_ranges (hdrs, 1000000, &ranges, &n_ranges));
++
++ soup_message_headers_free_ranges (hdrs, ranges);
++ soup_message_headers_unref (hdrs);
++ }
++}
++
+ #ifdef HAVE_APACHE
+ static void
+ do_apache_range_test (void)
+@@ -473,6 +774,49 @@ server_handler (SoupServer *server,
+ full_response);
+ }
+
++static void
++do_libsoup_only_range_test (SoupSession *session, const char *uri)
++{
++ gsize full_response_length = g_bytes_get_size (full_response);
++ GString *range;
++ int i;
++
++ /* A suffix length at least as long as the body selects the whole body. */
++ debug_printf (1, "Requesting (suffix range the length of the body) -%d\n",
++ (int) full_response_length);
++ request_single_range (session, uri,
++ -((int) full_response_length), -1,
++ SOUP_STATUS_PARTIAL_CONTENT, 0, -1);
++
++ debug_printf (1, "Requesting (suffix range longer than the body) -999999\n");
++ request_single_range_by_string_full (session, uri, "bytes=-999999",
++ SOUP_STATUS_PARTIAL_CONTENT, 0, -1);
++
++ debug_printf (1, "Requesting (suffix range overflowing gint64) -99999999999999999999\n");
++ request_single_range_by_string_full (session, uri, "bytes=-99999999999999999999",
++ SOUP_STATUS_PARTIAL_CONTENT, 0, -1);
++
++ /* A start which overflows gint64 is treated like any other start past
++ * the end of the body.
++ * https://gitlab.gnome.org/GNOME/libsoup/-/issues/535
++ */
++ debug_printf (1, "Requesting (start overflowing gint64) 9888888888888019900-\n");
++ request_single_range_by_string (session, uri, "bytes=9888888888888019900-",
++ SOUP_STATUS_OK);
++
++ /* More ranges than the server is willing to coalesce, which is
++ * rejected rather than answered with the whole body.
++ * https://gitlab.gnome.org/GNOME/libsoup/-/issues/538
++ */
++ debug_printf (1, "Requesting (more ranges than the limit)\n");
++ range = g_string_new ("bytes=");
++ for (i = 0; i < MAX_RANGES + 1; i++)
++ g_string_append (range, i > 0 ? ",0-0" : "0-0");
++ request_single_range_by_string (session, uri, range->str,
++ SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE);
++ g_string_free (range, TRUE);
++}
++
+ static void
+ do_libsoup_range_test (void)
+ {
+@@ -488,6 +832,7 @@ do_libsoup_range_test (void)
+ base_uri = soup_test_server_get_uri (server, "http", NULL);
+ base_uri_str = g_uri_to_string (base_uri);
+ do_range_test (session, base_uri_str, TRUE, TRUE);
++ do_libsoup_only_range_test (session, base_uri_str);
+ g_uri_unref (base_uri);
+ g_free (base_uri_str);
+ soup_test_server_quit_unref (server);
+@@ -512,6 +857,8 @@ main (int argc, char **argv)
+ g_test_add_func ("/ranges/apache", do_apache_range_test);
+ #endif
+ g_test_add_func ("/ranges/libsoup", do_libsoup_range_test);
++ g_test_add_func ("/ranges/parsing", do_range_parsing_test);
++ g_test_add_func ("/ranges/count", do_range_count_test);
+
+ ret = g_test_run ();
+
+diff --git a/tests/server-mem-limit-test.c b/tests/server-mem-limit-test.c
+index 65dc875e..ee20ba9b 100644
+--- a/tests/server-mem-limit-test.c
++++ b/tests/server-mem-limit-test.c
+@@ -4,6 +4,7 @@
+ */
+
+ #include "test-utils.h"
++#include "soup-message-headers-private.h"
+
+ #include <sys/resource.h>
+
+@@ -81,46 +82,66 @@ server_file_callback (SoupServer *server,
+ }
+
+ static void
+-do_ranges_overlaps_test (ServerData *sd, gconstpointer test_data)
++request_ranges (ServerData *sd, const char *range, SoupStatus expected_status)
+ {
+ SoupSession *session;
+ SoupMessage *msg;
+- GString *range;
+ GUri *uri;
+- const char *chunk = ",0,0,0,0,0,0,0,0,0,0,0";
+-
+- g_test_bug ("428");
+-
+- #ifdef G_OS_WIN32
+- g_test_skip ("Cannot run under windows");
+- return;
+- #endif
+-
+- range = g_string_sized_new (99 * 1024);
+- g_string_append (range, "bytes=1024");
+- while (range->len < 99 * 1024)
+- g_string_append (range, chunk);
+
+ session = soup_test_session_new (NULL);
+- server_add_handler (sd, "/file", server_file_callback, NULL, NULL);
+
+ uri = g_uri_parse_relative (sd->base_uri, "/file", SOUP_HTTP_URI_FLAGS, NULL);
+
+ msg = soup_message_new_from_uri ("GET", uri);
+- soup_message_headers_append (soup_message_get_request_headers (msg), "Range", range->str);
++ soup_message_headers_append (soup_message_get_request_headers (msg), "Range", range);
+
+ soup_test_session_send_message (session, msg);
+
+- soup_test_assert_message_status (msg, SOUP_STATUS_PARTIAL_CONTENT);
++ soup_test_assert_message_status (msg, expected_status);
+
+ g_object_unref (msg);
+-
+- g_string_free (range, TRUE);
+ g_uri_unref (uri);
+
+ soup_test_session_abort_unref (session);
+ }
+
++static void
++do_ranges_overlaps_test (ServerData *sd, gconstpointer test_data)
++{
++ GString *range;
++ const char *chunk = ",0,0,0,0,0,0,0,0,0,0,0";
++ int i;
++
++ g_test_bug ("428");
++
++ #ifdef G_OS_WIN32
++ g_test_skip ("Cannot run under windows");
++ return;
++ #endif
++
++ server_add_handler (sd, "/file", server_file_callback, NULL, NULL);
++
++ /* Requesting the same range many times over used to make the server
++ * allocate a response proportional to the number of ranges instead of
++ * coalescing them into one. Each "0" here is an open ended range
++ * covering the whole body, so they all collapse into a single range.
++ */
++ range = g_string_new ("bytes=1024");
++ for (i = 1; i < MAX_RANGES; i++)
++ g_string_append (range, ",0");
++ request_ranges (sd, range->str, SOUP_STATUS_PARTIAL_CONTENT);
++ g_string_free (range, TRUE);
++
++ /* A header listing more ranges than the server is willing to coalesce
++ * is rejected outright. */
++ range = g_string_sized_new ((gsize)99 * 1024);
++ g_string_append (range, "bytes=1024");
++ while (range->len < (gssize)99 * 1024)
++ g_string_append (range, chunk);
++ request_ranges (sd, range->str, SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE);
++ g_string_free (range, TRUE);
++}
++
+ int
+ main (int argc, char **argv)
+ {
+--
+GitLab
+
diff --git a/libsoup3.spec b/libsoup3.spec
index 6c3cd7f..ae1bbeb 100644
--- a/libsoup3.spec
+++ b/libsoup3.spec
@@ -21,6 +21,13 @@ Patch: no-ntlm-in-fips-mode.patch
# https://gitlab.gnome.org/GNOME/libsoup/-/work_items/530
Patch: skip-logger-test-on-32bit.patch
+Patch: CVE-2026-77680.patch
+Patch: CVE-2026-15711.patch
+Patch: CVE-2026-15714.patch
+Patch: CVE-2026-15709.patch
+Patch: CVE-2026-15713.patch
+Patch: CVE-2026-15712.patch
+
BuildRequires: gcc
BuildRequires: gettext
reply other threads:[~2026-09-05 1:22 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=178857137424.1.6571316064283254185.rpms-libsoup3-f2b42d21480e@fedoraproject.org \
--to=avovk@redhat.com \
--cc=git-commits@fedoraproject.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox