diff --git a/CVE-2025-14523.patch b/CVE-2025-14523.patch new file mode 100644 index 0000000..b795345 --- /dev/null +++ b/CVE-2025-14523.patch @@ -0,0 +1,695 @@ +From 3c975f8d53df061538f865edfe35f9dd4b3e2f80 Mon Sep 17 00:00:00 2001 +From: Michael Catanzaro +Date: Wed, 7 Jan 2026 14:50:33 -0600 +Subject: [PATCH] Reject duplicate Host headers + +RFC 9112 section 3.2 says: + +A server MUST respond with a 400 (Bad Request) status code to any +HTTP/1.1 request message that lacks a Host header field and to any +request message that contains more than one Host header field line or a +Host header field with an invalid field value. + +In addition to rejecting a duplicate header when parsing headers, also +reject attempts to add the duplicate header using the +soup_message_headers_append() API, and add tests for both cases. + +These checks will also apply to HTTP/2. I'm not sure whether this is +actually desired or not, but the header processing code is not aware of +which HTTP version is in use. + +(Note that while SoupMessageHeaders does not require the Host header to +be present in an HTTP/1.1 request, SoupServer itself does. So we can't +test the case of missing Host header via the header parsing test, but it +really is enforced.) + +Fixes #472 +--- + libsoup/soup-headers.c | 3 +- + libsoup/soup-message-headers-private.h | 4 +- + libsoup/soup-message-headers.c | 78 +++++++++------ + tests/header-parsing-test.c | 133 ++++++++++++++++--------- + 4 files changed, 137 insertions(+), 81 deletions(-) + +diff --git a/libsoup/soup-headers.c b/libsoup/soup-headers.c +index 52ef2ece..9dcffbf2 100644 +--- a/libsoup/soup-headers.c ++++ b/libsoup/soup-headers.c +@@ -139,7 +139,8 @@ soup_headers_parse (const char *str, int len, SoupMessageHeaders *dest) + for (p = strchr (value, '\r'); p; p = strchr (p, '\r')) + *p = ' '; + +- soup_message_headers_append_untrusted_data (dest, name, value); ++ if (!soup_message_headers_append_untrusted_data (dest, name, value)) ++ goto done; + } + success = TRUE; + +diff --git a/libsoup/soup-message-headers-private.h b/libsoup/soup-message-headers-private.h +index 98154645..770f3ef1 100644 +--- a/libsoup/soup-message-headers-private.h ++++ b/libsoup/soup-message-headers-private.h +@@ -10,10 +10,10 @@ + + G_BEGIN_DECLS + +-void soup_message_headers_append_untrusted_data (SoupMessageHeaders *hdrs, ++gboolean soup_message_headers_append_untrusted_data (SoupMessageHeaders *hdrs, + const char *name, + const char *value); +-void soup_message_headers_append_common (SoupMessageHeaders *hdrs, ++gboolean soup_message_headers_append_common (SoupMessageHeaders *hdrs, + SoupHeaderName name, + const char *value); + const char *soup_message_headers_get_one_common (SoupMessageHeaders *hdrs, +diff --git a/libsoup/soup-message-headers.c b/libsoup/soup-message-headers.c +index f101d4b4..b0d5828f 100644 +--- a/libsoup/soup-message-headers.c ++++ b/libsoup/soup-message-headers.c +@@ -273,12 +273,16 @@ soup_message_headers_clean_connection_headers (SoupMessageHeaders *hdrs) + soup_header_free_list (tokens); + } + +-void ++gboolean + soup_message_headers_append_common (SoupMessageHeaders *hdrs, + SoupHeaderName name, + const char *value) + { + SoupCommonHeader header; ++ if (name == SOUP_HEADER_HOST && soup_message_headers_get_one (hdrs, "Host")) { ++ g_warning ("Attempted to add duplicate Host header to a SoupMessageHeaders that already contains a Host header"); ++ return FALSE; ++ } + + if (!hdrs->common_headers) + hdrs->common_headers = g_array_sized_new (FALSE, FALSE, sizeof (SoupCommonHeader), 6); +@@ -290,33 +294,19 @@ soup_message_headers_append_common (SoupMessageHeaders *hdrs, + g_hash_table_remove (hdrs->common_concat, GUINT_TO_POINTER (header.name)); + + soup_message_headers_set (hdrs, name, value); ++ return TRUE; + } + +-/** +- * soup_message_headers_append: +- * @hdrs: a #SoupMessageHeaders +- * @name: the header name to add +- * @value: the new value of @name +- * +- * Appends a new header with name @name and value @value to @hdrs. +- * +- * (If there is an existing header with name @name, then this creates a second +- * one, which is only allowed for list-valued headers; see also +- * [method@MessageHeaders.replace].) +- * +- * The caller is expected to make sure that @name and @value are +- * syntactically correct. +- **/ +-void +-soup_message_headers_append (SoupMessageHeaders *hdrs, +- const char *name, const char *value) ++static gboolean ++soup_message_headers_append_internal (SoupMessageHeaders *hdrs, ++ const char *name, const char *value) + { + SoupUncommonHeader header; + SoupHeaderName header_name; + +- g_return_if_fail (hdrs); +- g_return_if_fail (name != NULL); +- g_return_if_fail (value != NULL); ++ g_return_val_if_fail (hdrs, FALSE); ++ g_return_val_if_fail (name != NULL, FALSE); ++ g_return_val_if_fail (value != NULL, FALSE); + + /* Setting a syntactically invalid header name or value is + * considered to be a programming error. However, it can also +@@ -324,23 +314,22 @@ soup_message_headers_append (SoupMessageHeaders *hdrs, + * compiled with G_DISABLE_CHECKS. + */ + #ifndef G_DISABLE_CHECKS +- g_return_if_fail (*name && strpbrk (name, " \t\r\n:") == NULL); +- g_return_if_fail (strpbrk (value, "\r\n") == NULL); ++ g_return_val_if_fail (*name && strpbrk (name, " \t\r\n:") == NULL, FALSE); ++ g_return_val_if_fail (strpbrk (value, "\r\n") == NULL, FALSE); + #else + if (*name && strpbrk (name, " \t\r\n:")) { + g_warning ("soup_message_headers_append: Ignoring bad name '%s'", name); +- return; ++ return FALSE; + } + if (strpbrk (value, "\r\n")) { + g_warning ("soup_message_headers_append: Ignoring bad value '%s'", value); +- return; ++ return FALSE; + } + #endif + + header_name = soup_header_name_from_string (name); + if (header_name != SOUP_HEADER_UNKNOWN) { +- soup_message_headers_append_common (hdrs, header_name, value); +- return; ++ return soup_message_headers_append_common (hdrs, header_name, value); + } + + if (!hdrs->uncommon_headers) +@@ -351,21 +340,48 @@ soup_message_headers_append (SoupMessageHeaders *hdrs, + g_array_append_val (hdrs->uncommon_headers, header); + if (hdrs->uncommon_concat) + g_hash_table_remove (hdrs->uncommon_concat, header.name); ++ return TRUE; ++} ++ ++/** ++ * soup_message_headers_append: ++ * @hdrs: a #SoupMessageHeaders ++ * @name: the header name to add ++ * @value: the new value of @name ++ * ++ * Appends a new header with name @name and value @value to @hdrs. ++ * ++ * (If there is an existing header with name @name, then this creates a second ++ * one, which is only allowed for list-valued headers; see also ++ * [method@MessageHeaders.replace].) ++ * ++ * The caller is expected to make sure that @name and @value are ++ * syntactically correct. ++ **/ ++void ++soup_message_headers_append (SoupMessageHeaders *hdrs, ++ const char *name, const char *value) ++{ ++ soup_message_headers_append_internal (hdrs, name, value); + } + + /* +- * Appends a header value ensuring that it is valid UTF8. ++ * Appends a header value ensuring that it is valid UTF-8, and also checking the ++ * return value of soup_message_headers_append_internal() to report whether the ++ * headers are invalid for various other reasons. + */ +-void ++gboolean + soup_message_headers_append_untrusted_data (SoupMessageHeaders *hdrs, + const char *name, + const char *value) + { + char *safe_value = g_utf8_make_valid (value, -1); + char *safe_name = g_utf8_make_valid (name, -1); +- soup_message_headers_append (hdrs, safe_name, safe_value); ++ gboolean result = soup_message_headers_append_internal (hdrs, safe_name, safe_value); ++ + g_free (safe_value); + g_free (safe_name); ++ return result; + } + + void +diff --git a/tests/header-parsing-test.c b/tests/header-parsing-test.c +index 4faafbd6..ea42d5e6 100644 +--- a/tests/header-parsing-test.c ++++ b/tests/header-parsing-test.c +@@ -24,6 +24,7 @@ static struct RequestTest { + const char *method, *path; + SoupHTTPVersion version; + Header headers[10]; ++ GLogLevelFlags log_flags; + } reqtests[] = { + /**********************/ + /*** VALID REQUESTS ***/ +@@ -33,7 +34,7 @@ static struct RequestTest { + "GET / HTTP/1.0\r\n", -1, + SOUP_STATUS_OK, + "GET", "/", SOUP_HTTP_1_0, +- { { NULL } } ++ { { NULL } }, 0 + }, + + { "Req w/ 1 header", NULL, +@@ -42,7 +43,7 @@ static struct RequestTest { + "GET", "/", SOUP_HTTP_1_1, + { { "Host", "example.com" }, + { NULL } +- } ++ }, 0 + }, + + { "Req w/ 1 header, no leading whitespace", NULL, +@@ -51,7 +52,7 @@ static struct RequestTest { + "GET", "/", SOUP_HTTP_1_1, + { { "Host", "example.com" }, + { NULL } +- } ++ }, 0 + }, + + { "Req w/ 1 header including trailing whitespace", NULL, +@@ -60,7 +61,7 @@ static struct RequestTest { + "GET", "/", SOUP_HTTP_1_1, + { { "Host", "example.com" }, + { NULL } +- } ++ }, 0 + }, + + { "Req w/ 1 header, wrapped", NULL, +@@ -69,7 +70,7 @@ static struct RequestTest { + "GET", "/", SOUP_HTTP_1_1, + { { "Foo", "bar baz" }, + { NULL } +- } ++ }, 0 + }, + + { "Req w/ 1 header, wrapped with additional whitespace", NULL, +@@ -78,7 +79,7 @@ static struct RequestTest { + "GET", "/", SOUP_HTTP_1_1, + { { "Foo", "bar baz" }, + { NULL } +- } ++ }, 0 + }, + + { "Req w/ 1 header, wrapped with tab", NULL, +@@ -87,7 +88,7 @@ static struct RequestTest { + "GET", "/", SOUP_HTTP_1_1, + { { "Foo", "bar baz" }, + { NULL } +- } ++ }, 0 + }, + + { "Req w/ 1 header, wrapped before value", NULL, +@@ -96,7 +97,7 @@ static struct RequestTest { + "GET", "/", SOUP_HTTP_1_1, + { { "Foo", "bar baz" }, + { NULL } +- } ++ }, 0 + }, + + { "Req w/ 1 header with empty value", NULL, +@@ -105,7 +106,7 @@ static struct RequestTest { + "GET", "/", SOUP_HTTP_1_1, + { { "Host", "" }, + { NULL } +- } ++ }, 0 + }, + + { "Req w/ 2 headers", NULL, +@@ -115,7 +116,7 @@ static struct RequestTest { + { { "Host", "example.com" }, + { "Connection", "close" }, + { NULL } +- } ++ }, 0 + }, + + { "Req w/ 3 headers", NULL, +@@ -126,7 +127,7 @@ static struct RequestTest { + { "Connection", "close" }, + { "Blah", "blah" }, + { NULL } +- } ++ }, 0 + }, + + { "Req w/ 3 headers, 1st wrapped", NULL, +@@ -137,7 +138,7 @@ static struct RequestTest { + { "Foo", "bar baz" }, + { "Blah", "blah" }, + { NULL } +- } ++ }, 0 + }, + + { "Req w/ 3 headers, 2nd wrapped", NULL, +@@ -148,7 +149,7 @@ static struct RequestTest { + { "Blah", "blah" }, + { "Foo", "bar baz" }, + { NULL } +- } ++ }, 0 + }, + + { "Req w/ 3 headers, 3rd wrapped", NULL, +@@ -159,7 +160,7 @@ static struct RequestTest { + { "Blah", "blah" }, + { "Foo", "bar baz" }, + { NULL } +- } ++ }, 0 + }, + + { "Req w/ same header multiple times", NULL, +@@ -168,7 +169,7 @@ static struct RequestTest { + "GET", "/", SOUP_HTTP_1_1, + { { "Foo", "bar, baz, quux" }, + { NULL } +- } ++ }, 0 + }, + + { "Connection header on HTTP/1.0 message", NULL, +@@ -178,21 +179,21 @@ static struct RequestTest { + { { "Connection", "Bar, Quux" }, + { "Foo", "bar" }, + { NULL } +- } ++ }, 0 + }, + + { "GET with full URI", "667637", + "GET http://example.com HTTP/1.1\r\n", -1, + SOUP_STATUS_OK, + "GET", "http://example.com", SOUP_HTTP_1_1, +- { { NULL } } ++ { { NULL } }, 0 + }, + + { "GET with full URI in upper-case", "667637", + "GET HTTP://example.com HTTP/1.1\r\n", -1, + SOUP_STATUS_OK, + "GET", "HTTP://example.com", SOUP_HTTP_1_1, +- { { NULL } } ++ { { NULL } }, 0 + }, + + /* It's better for this to be passed through: this means a SoupServer +@@ -202,7 +203,7 @@ static struct RequestTest { + "GET AbOuT: HTTP/1.1\r\n", -1, + SOUP_STATUS_OK, + "GET", "AbOuT:", SOUP_HTTP_1_1, +- { { NULL } } ++ { { NULL } }, 0 + }, + + /****************************/ +@@ -217,7 +218,7 @@ static struct RequestTest { + "GET", "/", SOUP_HTTP_1_1, + { { "Host", "example.com" }, + { NULL } +- } ++ }, 0 + }, + + /* RFC 2616 section 3.1 says we MUST accept this */ +@@ -228,7 +229,7 @@ static struct RequestTest { + "GET", "/", SOUP_HTTP_1_1, + { { "Host", "example.com" }, + { NULL } +- } ++ }, 0 + }, + + /* RFC 2616 section 19.3 says we SHOULD accept these */ +@@ -240,7 +241,7 @@ static struct RequestTest { + { { "Host", "example.com" }, + { "Connection", "close" }, + { NULL } +- } ++ }, 0 + }, + + { "LF instead of CRLF after Request-Line", NULL, +@@ -249,7 +250,7 @@ static struct RequestTest { + "GET", "/", SOUP_HTTP_1_1, + { { "Host", "example.com" }, + { NULL } +- } ++ }, 0 + }, + + { "Mixed CRLF/LF", "666316", +@@ -261,7 +262,7 @@ static struct RequestTest { + { "e", "f" }, + { "g", "h" }, + { NULL } +- } ++ }, 0 + }, + + { "Req w/ incorrect whitespace in Request-Line", NULL, +@@ -270,7 +271,7 @@ static struct RequestTest { + "GET", "/", SOUP_HTTP_1_1, + { { "Host", "example.com" }, + { NULL } +- } ++ }, 0 + }, + + { "Req w/ incorrect whitespace after Request-Line", "475169", +@@ -279,7 +280,7 @@ static struct RequestTest { + "GET", "/", SOUP_HTTP_1_1, + { { "Host", "example.com" }, + { NULL } +- } ++ }, 0 + }, + + /* If the request/status line is parseable, then we +@@ -293,7 +294,7 @@ static struct RequestTest { + { { "Host", "example.com" }, + { "Bar", "two" }, + { NULL } +- } ++ }, 0 + }, + + { "First header line is continuation", "666316", +@@ -303,7 +304,7 @@ static struct RequestTest { + { { "Host", "example.com" }, + { "c", "d" }, + { NULL } +- } ++ }, 0 + }, + + { "Zero-length header name", "666316", +@@ -313,7 +314,7 @@ static struct RequestTest { + { { "a", "b" }, + { "c", "d" }, + { NULL } +- } ++ }, 0 + }, + + { "CR in header name", "666316", +@@ -323,7 +324,7 @@ static struct RequestTest { + { { "a", "b" }, + { "c", "d" }, + { NULL } +- } ++ }, 0 + }, + + { "CR in header value", "666316", +@@ -336,7 +337,7 @@ static struct RequestTest { + { "s", "t" }, /* CR at end is ignored */ + { "c", "d" }, + { NULL } +- } ++ }, 0 + }, + + { "Tab in header name", "666316", +@@ -351,7 +352,7 @@ static struct RequestTest { + { "p", "q z: w" }, + { "c", "d" }, + { NULL } +- } ++ }, 0 + }, + + { "Tab in header value", "666316", +@@ -364,7 +365,7 @@ static struct RequestTest { + { "z", "w" }, /* trailing tab ignored */ + { "c", "d" }, + { NULL } +- } ++ }, 0 + }, + + /************************/ +@@ -375,77 +376,77 @@ static struct RequestTest { + "GET /\r\n", -1, + SOUP_STATUS_BAD_REQUEST, + NULL, NULL, -1, +- { { NULL } } ++ { { NULL } }, 0 + }, + + { "HTTP 1.2 request (no such thing)", NULL, + "GET / HTTP/1.2\r\n", -1, + SOUP_STATUS_HTTP_VERSION_NOT_SUPPORTED, + NULL, NULL, -1, +- { { NULL } } ++ { { NULL } }, 0 + }, + + { "HTTP 2000 request (no such thing)", NULL, + "GET / HTTP/2000.0\r\n", -1, + SOUP_STATUS_HTTP_VERSION_NOT_SUPPORTED, + NULL, NULL, -1, +- { { NULL } } ++ { { NULL } }, 0 + }, + + { "Long HTTP version terminating at missing minor version", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/404", + unterminated_http_version, sizeof (unterminated_http_version), + SOUP_STATUS_BAD_REQUEST, + NULL, NULL, -1, +- { { NULL } } ++ { { NULL } }, 0 + }, + + { "Non-HTTP request", NULL, + "GET / SOUP/1.1\r\nHost: example.com\r\n", -1, + SOUP_STATUS_BAD_REQUEST, + NULL, NULL, -1, +- { { NULL } } ++ { { NULL } }, 0 + }, + + { "Junk after Request-Line", NULL, + "GET / HTTP/1.1 blah\r\nHost: example.com\r\n", -1, + SOUP_STATUS_BAD_REQUEST, + NULL, NULL, -1, +- { { NULL } } ++ { { NULL } }, 0 + }, + + { "NUL in Method", NULL, + "G\x00T / HTTP/1.1\r\nHost: example.com\r\n", 37, + SOUP_STATUS_BAD_REQUEST, + NULL, NULL, -1, +- { { NULL } } ++ { { NULL } }, 0 + }, + + { "NUL at beginning of Method", "666316", + "\x00 / HTTP/1.1\r\nHost: example.com\r\n", 35, + SOUP_STATUS_BAD_REQUEST, + NULL, NULL, -1, +- { { NULL } } ++ { { NULL } }, 0 + }, + + { "NUL in Path", NULL, + "GET /\x00 HTTP/1.1\r\nHost: example.com\r\n", 38, + SOUP_STATUS_BAD_REQUEST, + NULL, NULL, -1, +- { { NULL } } ++ { { NULL } }, 0 + }, + + { "No terminating CRLF", NULL, + "GET / HTTP/1.1\r\nHost: example.com", -1, + SOUP_STATUS_BAD_REQUEST, + NULL, NULL, -1, +- { { NULL } } ++ { { NULL } }, 0 + }, + + { "Unrecognized expectation", NULL, + "GET / HTTP/1.1\r\nHost: example.com\r\nExpect: the-impossible\r\n", -1, + SOUP_STATUS_EXPECTATION_FAILED, + NULL, NULL, -1, +- { { NULL } } ++ { { NULL } }, 0 + }, + + // https://gitlab.gnome.org/GNOME/libsoup/-/issues/377 +@@ -453,21 +454,31 @@ static struct RequestTest { + "GET / HTTP/1.1\r\nHost\x00: example.com\r\n", 36, + SOUP_STATUS_BAD_REQUEST, + NULL, NULL, -1, +- { { NULL } } ++ { { NULL } }, 0 + }, + + { "NUL in header value", NULL, + "HTTP/1.1 200 OK\r\nFoo: b\x00" "ar\r\n", 28, + SOUP_STATUS_BAD_REQUEST, + NULL, NULL, -1, +- { { NULL } } ++ { { NULL } }, 0 + }, + + { "Only newlines", NULL, + only_newlines, sizeof (only_newlines), + SOUP_STATUS_BAD_REQUEST, + NULL, NULL, -1, +- { { NULL } } ++ { { NULL } }, 0 ++ }, ++ ++ { "Duplicate Host headers", ++ "https://gitlab.gnome.org/GNOME/libsoup/-/issues/472", ++ "GET / HTTP/1.1\r\nHost: example.com\r\nHost: example.org\r\n", ++ -1, ++ SOUP_STATUS_BAD_REQUEST, ++ NULL, NULL, -1, ++ { { NULL } }, ++ G_LOG_LEVEL_WARNING + } + }; + static const int num_reqtests = G_N_ELEMENTS (reqtests); +@@ -915,10 +926,17 @@ do_request_tests (void) + len = strlen (reqtests[i].request); + else + len = reqtests[i].length; ++ ++ if (reqtests[i].log_flags) ++ g_test_expect_message ("libsoup", reqtests[i].log_flags, "*"); ++ + status = soup_headers_parse_request (reqtests[i].request, len, + headers, &method, &path, + &version); + g_assert_cmpint (status, ==, reqtests[i].status); ++ if (reqtests[i].log_flags) ++ g_test_assert_expected_messages (); ++ + if (SOUP_STATUS_IS_SUCCESSFUL (status)) { + g_assert_cmpstr (method, ==, reqtests[i].method); + g_assert_cmpstr (path, ==, reqtests[i].path); +@@ -1314,6 +1332,25 @@ do_bad_header_tests (void) + soup_message_headers_unref (hdrs); + } + ++static void ++do_append_duplicate_host_test (void) ++{ ++ SoupMessageHeaders *hdrs; ++ const char *list_value; ++ ++ hdrs = soup_message_headers_new (SOUP_MESSAGE_HEADERS_REQUEST); ++ soup_message_headers_append (hdrs, "Host", "a"); ++ g_test_expect_message ("libsoup", G_LOG_LEVEL_WARNING, ++ "Attempted to add duplicate Host header to a SoupMessageHeaders that already contains a Host header"); ++ soup_message_headers_append (hdrs, "Host", "b"); ++ g_test_assert_expected_messages (); ++ ++ list_value = soup_message_headers_get_list (hdrs, "Host"); ++ g_assert_cmpstr (list_value, ==, "a"); ++ ++ soup_message_headers_unref (hdrs); ++} ++ + int + main (int argc, char **argv) + { +@@ -1330,6 +1367,8 @@ main (int argc, char **argv) + g_test_add_func ("/header-parsing/append-param", do_append_param_tests); + g_test_add_func ("/header-parsing/bad", do_bad_header_tests); + ++ g_test_add_func ("/header-parsing/append-duplicate-host", do_append_duplicate_host_test); ++ + ret = g_test_run (); + + test_cleanup (); +-- +2.52.0 + diff --git a/libsoup3.spec b/libsoup3.spec index d09ed1c..fd856ed 100644 --- a/libsoup3.spec +++ b/libsoup3.spec @@ -32,6 +32,8 @@ Patch: CVE-2025-32907.patch Patch: CVE-2025-4945-CVE-2025-11021.patch # https://gitlab.gnome.org/GNOME/libsoup/-/merge_requests/481 Patch: CVE-2025-12105.patch +# https://gitlab.gnome.org/GNOME/libsoup/-/merge_requests/491 +Patch: CVE-2025-14523.patch BuildRequires: ca-certificates BuildRequires: gcc