From 7bb3115a296154e3f465900ea5c984a493385a7f Mon Sep 17 00:00:00 2001
From: Philip Withnall <pwithnall@gnome.org>
Date: Fri, 19 Dec 2025 23:49:05 +0000
Subject: [PATCH] Fix CVE-2026-2443

Upstream-Status: Backport [
c1796442 soup-message-headers: Rework Range response statuses to match Apache
191ef313 soup-message-headers: Fix rejection of Range headers with trailing garbage
be677bea soup-message-headers: Fix parsing of invalid Range suffix lengths
2bbfdfe8 soup-message-headers: Reject ranges where end is before start
739bf7cb soup-message-headers: Reject invalid Range ends longer than the content
]
CVE: CVE-2026-2443

Signed-off-by: Changqing Li <changqing.li@windriver.com>
---
 libsoup/soup-message-headers.c | 62 ++++++++++++++++++++++++----------
 1 file changed, 44 insertions(+), 18 deletions(-)

diff --git a/libsoup/soup-message-headers.c b/libsoup/soup-message-headers.c
index bb20bbb..535cf14 100644
--- a/libsoup/soup-message-headers.c
+++ b/libsoup/soup-message-headers.c
@@ -943,10 +943,16 @@ sort_ranges (gconstpointer a, gconstpointer b)
 }
 
 /* like soup_message_headers_get_ranges(), except it returns:
- *   SOUP_STATUS_OK if there is no Range or it should be ignored.
- *   SOUP_STATUS_PARTIAL_CONTENT if there is at least one satisfiable range.
- *   SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE if @check_satisfiable
- *     is %TRUE and the request is not satisfiable given @total_length.
+ *  - SOUP_STATUS_OK if there is no Range or it should be ignored due to being
+ *    entirely invalid.
+ *  - SOUP_STATUS_PARTIAL_CONTENT if there is at least one satisfiable range.
+ *  - SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE if @check_satisfiable
+ *     is %TRUE, the Range is valid, but no part of the request is satisfiable
+ *     given @total_length.
+ *
+ * @ranges and @length are only set if SOUP_STATUS_PARTIAL_CONTENT is returned.
+ *
+ * See https://httpwg.org/specs/rfc9110.html#field.range
  */
 guint
 soup_message_headers_get_ranges_internal (SoupMessageHeaders  *hdrs,
@@ -960,22 +966,28 @@ soup_message_headers_get_ranges_internal (SoupMessageHeaders  *hdrs,
 	GArray *array;
 	char *spec, *end;
 	guint status = SOUP_STATUS_OK;
+	gboolean is_all_valid = TRUE;
 
 	if (!range || strncmp (range, "bytes", 5) != 0)
-		return status;
+		return SOUP_STATUS_OK;  /* invalid header or unknown range unit */
 
 	range += 5;
 	while (g_ascii_isspace (*range))
 		range++;
 	if (*range++ != '=')
-		return status;
+		return SOUP_STATUS_OK;  /* invalid header */
 	while (g_ascii_isspace (*range))
 		range++;
 
 	range_list = soup_header_parse_list (range);
 	if (!range_list)
-		return status;
+		return SOUP_STATUS_OK;  /* invalid list */
 
+	/* 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
+	 * status 416 (Range Not Satisfiable) if there are no partially valid
+	 * ranges at all. */
 	array = g_array_new (FALSE, FALSE, sizeof (SoupRange));
 	for (r = range_list; r; r = r->next) {
 		SoupRange cur;
@@ -988,30 +1000,44 @@ soup_message_headers_get_ranges_internal (SoupMessageHeaders  *hdrs,
 			cur.start = g_ascii_strtoull (spec, &end, 10);
 			if (*end == '-')
 				end++;
-			if (*end) {
+			if (*end)
 				cur.end = g_ascii_strtoull (end, &end, 10);
-				if (cur.end < cur.start) {
-					status = SOUP_STATUS_OK;
-					break;
-				}
-			} else
+			else
 				cur.end = total_length - 1;
 		}
+
 		if (*end) {
-			status = SOUP_STATUS_OK;
-			break;
-		} else if (check_satisfiable && cur.start >= total_length) {
-			if (status == SOUP_STATUS_OK)
-				status = SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE;
+			/* Junk after the range */
+			is_all_valid = FALSE;
+			continue;
+		}
+
+		if (cur.end < cur.start) {
+			is_all_valid = FALSE;
+			continue;
+		}
+
+		g_assert (cur.start >= 0);
+		if (cur.end >= total_length)
+			cur.end = total_length - 1;
+
+		if (cur.start >= total_length) {
+			/* Range is valid, but unsatisfiable */
 			continue;
 		}
 
+		/* We have at least one (at least partially) satisfiable range */
 		g_array_append_val (array, cur);
 		status = SOUP_STATUS_PARTIAL_CONTENT;
 	}
 	soup_header_free_list (range_list);
 
 	if (status != SOUP_STATUS_PARTIAL_CONTENT) {
+		g_assert (status == SOUP_STATUS_OK);
+
+		if (is_all_valid && check_satisfiable)
+			status = SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE;
+
 		g_array_free (array, TRUE);
 		return status;
 	}
-- 
2.34.1

