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