|
| 1 | +From d9d8d64f8002ce40742d95447c0c5fbe9dac76af Mon Sep 17 00:00:00 2001 |
| 2 | +From: AllSpark <allspark@microsoft.com> |
| 3 | +Date: Wed, 29 Apr 2026 18:28:19 +0000 |
| 4 | +Subject: [PATCH] Do not allow adding multiple content length values to headers |
| 5 | + |
| 6 | +Closes #500 |
| 7 | + |
| 8 | +Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com> |
| 9 | +Upstream-reference: AI Backport of https://gitlab.gnome.org/GNOME/libsoup/-/commit/e032d3e9b0a27d10597398023532dd8f9b6654cf.patch |
| 10 | + |
| 11 | +--- |
| 12 | + libsoup/soup-message-headers.c | 27 ++++++++++++++ |
| 13 | + tests/header-parsing-test.c | 50 ++++++++++++++++++++++++++ |
| 14 | + tests/server-test.c | 64 ++++++++++++++++++++++++++++++++++ |
| 15 | + 3 files changed, 141 insertions(+) |
| 16 | + |
| 17 | +diff --git a/libsoup/soup-message-headers.c b/libsoup/soup-message-headers.c |
| 18 | +index 41d56b8..236f64d 100644 |
| 19 | +--- a/libsoup/soup-message-headers.c |
| 20 | ++++ b/libsoup/soup-message-headers.c |
| 21 | +@@ -263,6 +263,33 @@ soup_message_headers_append_common (SoupMessageHeaders *hdrs, |
| 22 | + return FALSE; |
| 23 | + } |
| 24 | + |
| 25 | ++ if (name == SOUP_HEADER_CONTENT_LENGTH) { |
| 26 | ++ /* RFC 9110 - 7.7. Content-Length |
| 27 | ++ * If a message is received that has a Content-Length header field value consisting of |
| 28 | ++ * the same decimal value as a comma-separated list (Section 5.7.1) — for example, |
| 29 | ++ * "Content-Length: 42, 42" — indicating that duplicate Content-Length header fields have |
| 30 | ++ * been generated or combined by an upstream message processor, then the recipient must either |
| 31 | ++ * reject the message as invalid or replace the duplicated field values with a single valid |
| 32 | ++ * Content-Length field containing that decimal value prior to determining the message body |
| 33 | ++ * length or forwarding the message. |
| 34 | ++ */ |
| 35 | ++ const char *content_length = soup_message_headers_get_one_common (hdrs, SOUP_HEADER_CONTENT_LENGTH); |
| 36 | ++ if (content_length) { |
| 37 | ++ guint64 decimal_value1, decimal_value2; |
| 38 | ++ char *end; |
| 39 | ++ |
| 40 | ++ decimal_value1 = g_ascii_strtoull (content_length, &end, 10); |
| 41 | ++ if (*end) |
| 42 | ++ return FALSE; |
| 43 | ++ |
| 44 | ++ decimal_value2 = g_ascii_strtoull (value, &end, 10); |
| 45 | ++ if (*end) |
| 46 | ++ return FALSE; |
| 47 | ++ |
| 48 | ++ return decimal_value1 == decimal_value2; |
| 49 | ++ } |
| 50 | ++ } |
| 51 | ++ |
| 52 | + if (!trusted_value && !is_valid_header_value (value)) { |
| 53 | + g_warning ("soup_message_headers_append: Rejecting bad value '%s'", value); |
| 54 | + return FALSE; |
| 55 | +diff --git a/tests/header-parsing-test.c b/tests/header-parsing-test.c |
| 56 | +index 838baa6..55d47f8 100644 |
| 57 | +--- a/tests/header-parsing-test.c |
| 58 | ++++ b/tests/header-parsing-test.c |
| 59 | +@@ -359,6 +359,23 @@ static struct RequestTest { |
| 60 | + }, 0 |
| 61 | + }, |
| 62 | + |
| 63 | ++ { "Duplicate Content-Length with the same value", NULL, |
| 64 | ++ "POST / HTTP/1.1\r\nContent-Length: 4\r\nContent-Length: 4\r\n", |
| 65 | ++ -1, |
| 66 | ++ SOUP_STATUS_OK, |
| 67 | ++ "POST", "/", SOUP_HTTP_1_1, |
| 68 | ++ { { "Content-Length", "4" } }, 0 |
| 69 | ++ }, |
| 70 | ++ |
| 71 | ++ { "Duplicate Content-Length with the same decimal value", NULL, |
| 72 | ++ "POST / HTTP/1.1\r\nContent-Length: 04\r\nContent-Length: 4\r\n", |
| 73 | ++ -1, |
| 74 | ++ SOUP_STATUS_OK, |
| 75 | ++ "POST", "/", SOUP_HTTP_1_1, |
| 76 | ++ { { "Content-Length", "04" } }, 0 |
| 77 | ++ }, |
| 78 | ++ |
| 79 | ++ |
| 80 | + /************************/ |
| 81 | + /*** INVALID REQUESTS ***/ |
| 82 | + /************************/ |
| 83 | +@@ -448,6 +465,15 @@ static struct RequestTest { |
| 84 | + { { NULL } }, 0 |
| 85 | + }, |
| 86 | + |
| 87 | ++ { "Duplicate Content-Length with different value", |
| 88 | ++ "https://gitlab.gnome.org/GNOME/libsoup/-/issues/500", |
| 89 | ++ "POST / HTTP/1.1\r\nContent-Length: 2\r\nContent-Length: 4\r\n", |
| 90 | ++ -1, |
| 91 | ++ SOUP_STATUS_BAD_REQUEST, |
| 92 | ++ NULL, NULL, -1, |
| 93 | ++ { { NULL } }, 0 |
| 94 | ++ }, |
| 95 | ++ |
| 96 | + { "Duplicate Host headers", |
| 97 | + "https://gitlab.gnome.org/GNOME/libsoup/-/issues/472", |
| 98 | + "GET / HTTP/1.1\r\nHost: example.com\r\nHost: example.org\r\n", |
| 99 | +@@ -1373,6 +1399,28 @@ do_bad_header_tests (void) |
| 100 | + soup_message_headers_unref (hdrs); |
| 101 | + } |
| 102 | + |
| 103 | ++static void |
| 104 | ++do_append_duplicate_content_length_test (void) |
| 105 | ++{ |
| 106 | ++ SoupMessageHeaders *hdrs; |
| 107 | ++ const char *list_value; |
| 108 | ++ |
| 109 | ++ hdrs = soup_message_headers_new (SOUP_MESSAGE_HEADERS_REQUEST); |
| 110 | ++ soup_message_headers_append (hdrs, "Content-Length", "42"); |
| 111 | ++ |
| 112 | ++ /* Inserting the same value doesn't generate a list */ |
| 113 | ++ soup_message_headers_append (hdrs, "Content-Length", "42"); |
| 114 | ++ list_value = soup_message_headers_get_list (hdrs, "Content-Length"); |
| 115 | ++ g_assert_cmpstr (list_value, ==, "42"); |
| 116 | ++ |
| 117 | ++ /* Inserting a different value does nothing */ |
| 118 | ++ soup_message_headers_append (hdrs, "Content-Length", "45"); |
| 119 | ++ list_value = soup_message_headers_get_list (hdrs, "Content-Length"); |
| 120 | ++ g_assert_cmpstr (list_value, ==, "42"); |
| 121 | ++ |
| 122 | ++ soup_message_headers_unref (hdrs); |
| 123 | ++} |
| 124 | ++ |
| 125 | + static void |
| 126 | + do_append_duplicate_host_test (void) |
| 127 | + { |
| 128 | +@@ -1415,6 +1463,8 @@ main (int argc, char **argv) |
| 129 | + g_test_add_func ("/header-parsing/append-param", do_append_param_tests); |
| 130 | + g_test_add_func ("/header-parsing/bad", do_bad_header_tests); |
| 131 | + g_test_add_func ("/header-parsing/append-duplicate-host", do_append_duplicate_host_test); |
| 132 | ++ g_test_add_func ("/header-parsing/append-duplicate-content-length", do_append_duplicate_content_length_test); |
| 133 | ++ |
| 134 | + |
| 135 | + ret = g_test_run (); |
| 136 | + |
| 137 | +diff --git a/tests/server-test.c b/tests/server-test.c |
| 138 | +index 96fb428..858bd8c 100644 |
| 139 | +--- a/tests/server-test.c |
| 140 | ++++ b/tests/server-test.c |
| 141 | +@@ -1421,6 +1421,68 @@ do_chunked_test (ServerData *sd, gconstpointer test_data) |
| 142 | + g_object_unref (client); |
| 143 | + } |
| 144 | + } |
| 145 | ++static void |
| 146 | ++do_multiple_content_length_test (ServerData *sd, gconstpointer test_data) |
| 147 | ++{ |
| 148 | ++ gint i; |
| 149 | ++ struct { |
| 150 | ++ const char *description; |
| 151 | ++ const char *test; |
| 152 | ++ const char *expected_response; |
| 153 | ++ } tests[] = { |
| 154 | ++ { "Double Content-Length with different value", "POST / HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length: 0\r\nContent-Length: 4\r\nConnection: close\r\n\r\n\r\nABCD", "HTTP/1.0 400 Bad Request" }, |
| 155 | ++ { "Double Content-Length with the same value", "POST / HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length: 4\r\nContent-Length: 4\r\nConnection: close\r\n\r\n\r\nABCD", "HTTP/1.1 200 OK" }, |
| 156 | ++ }; |
| 157 | ++ |
| 158 | ++ sd->server = soup_test_server_new (SOUP_TEST_SERVER_IN_THREAD); |
| 159 | ++ sd->base_uri = soup_test_server_get_uri (sd->server, "http", NULL); |
| 160 | ++ server_add_handler (sd, NULL, server_callback, NULL, NULL); |
| 161 | ++ |
| 162 | ++ for (i = 0; i < G_N_ELEMENTS (tests); i++) { |
| 163 | ++ GSocketClient *client; |
| 164 | ++ GSocketConnection *conn; |
| 165 | ++ GInputStream *input; |
| 166 | ++ GOutputStream *output; |
| 167 | ++ gsize nwritten; |
| 168 | ++ char buffer[4096]; |
| 169 | ++ gssize nread; |
| 170 | ++ GString *response; |
| 171 | ++ const char *boundary; |
| 172 | ++ GError *error = NULL; |
| 173 | ++ |
| 174 | ++ debug_printf (1, " %s\n", tests[i].description); |
| 175 | ++ |
| 176 | ++ client = g_socket_client_new (); |
| 177 | ++ conn = g_socket_client_connect_to_host (client, g_uri_get_host (sd->base_uri), g_uri_get_port (sd->base_uri), NULL, &error); |
| 178 | ++ g_assert_no_error (error); |
| 179 | ++ |
| 180 | ++ output = g_io_stream_get_output_stream (G_IO_STREAM (conn)); |
| 181 | ++ g_output_stream_write_all (output, tests[i].test, strlen (tests[i].test), &nwritten, NULL, &error); |
| 182 | ++ g_assert_no_error (error); |
| 183 | ++ g_assert_cmpuint (nwritten, ==, strlen (tests[i].test)); |
| 184 | ++ g_output_stream_flush (output, NULL, &error); |
| 185 | ++ g_assert_no_error (error); |
| 186 | ++ |
| 187 | ++ response = g_string_new (NULL); |
| 188 | ++ |
| 189 | ++ input = g_io_stream_get_input_stream (G_IO_STREAM (conn)); |
| 190 | ++ do { |
| 191 | ++ nread = g_input_stream_read (input, buffer, sizeof(buffer), NULL, NULL); |
| 192 | ++ if (nread >= 0) |
| 193 | ++ response = g_string_append_len (response, (const char *)buffer, nread); |
| 194 | ++ } while (nread > 0); |
| 195 | ++ |
| 196 | ++ boundary = strstr (response->str, "\r\n"); |
| 197 | ++ g_assert_nonnull (boundary); |
| 198 | ++ response = g_string_truncate (response, response->len - strlen (boundary)); |
| 199 | ++ g_assert_cmpstr (response->str, ==, tests[i].expected_response); |
| 200 | ++ g_string_free (response, TRUE); |
| 201 | ++ |
| 202 | ++ g_object_unref (conn); |
| 203 | ++ g_object_unref (client); |
| 204 | ++ } |
| 205 | ++ |
| 206 | ++} |
| 207 | + |
| 208 | + int |
| 209 | + main (int argc, char **argv) |
| 210 | +@@ -1462,6 +1524,8 @@ main (int argc, char **argv) |
| 211 | + server_setup_nohandler, do_early_multi_test, server_teardown); |
| 212 | + g_test_add ("/server/steal/CONNECT", ServerData, NULL, |
| 213 | + server_setup, do_steal_connect_test, server_teardown); |
| 214 | ++ g_test_add ("/server/multiple-content-length", ServerData, NULL, |
| 215 | ++ NULL, do_multiple_content_length_test, server_teardown); |
| 216 | + g_test_add ("/server/chunked", ServerData, NULL, |
| 217 | + NULL, do_chunked_test, server_teardown); |
| 218 | + |
| 219 | +-- |
| 220 | +2.45.4 |
| 221 | + |
0 commit comments