From 96ac392b444d01bd5de1d1276b187c3ed49d048c Mon Sep 17 00:00:00 2001
From: Carlos Garcia Campos <cgarcia@igalia.com>
Date: Mon, 2 Mar 2026 14:03:41 +0100
Subject: [PATCH] server: improve parsing of chunked request body

Replying with 400 when failing to parse any chunked section and 413 when
the parsed chunked size is too large.

Closes #508

Conflict:libsoup/http1/soup-body-input-stream.c=>libsoup/soup-body-input-stream.c
         libsoup/http1/soup-server-message-io-http1.c=>libsoup/soup-message-io.c
         priv->read_length=>bistream->priv->read_length
         request_headers = soup_server_message_get_request_headers (msg);=>soup_message_headers_append (msg->request_headers, "Connection", "close");
         soup_message_set_status=>soup_server_message_set_status
         Context adapt
Reference:https://gitlab.gnome.org/GNOME/libsoup/-/commit/96ac392b444d01bd5de1d1276b187c3ed49d048c
---
 libsoup/soup-body-input-stream.c | 32 ++++++++++++++++++++++++++++++--
 libsoup/soup-message-io.c        | 15 +++++++++++++--
 tests/server-test.c              | 23 +++++++++++++++++++----
 3 files changed, 62 insertions(+), 8 deletions(-)

diff --git a/libsoup/soup-body-input-stream.c b/libsoup/soup-body-input-stream.c
index fb02bdb..8d17afe 100644
--- a/libsoup/soup-body-input-stream.c
+++ b/libsoup/soup-body-input-stream.c
@@ -154,6 +154,9 @@ soup_body_input_stream_read_chunked (SoupBodyInputStream  *bistream,
 	SoupFilterInputStream *fstream = SOUP_FILTER_INPUT_STREAM (bistream->priv->base_stream);
 	char metabuf[128];
 	gssize nread;
+	guint64 chunk_size;
+	gchar *end;
+	char *boundary;
 	gboolean got_line;
 
 again:
@@ -166,7 +169,7 @@ again:
 		if (nread < 0)
 			return nread;
 
-		if (nread == 0 || !got_line) {
+		if (nread == 0 || !got_line  || nread < 3) {
 			if (error && *error == NULL) {
 				g_set_error_literal (error, G_IO_ERROR,
 						     G_IO_ERROR_PARTIAL_INPUT,
@@ -175,7 +178,32 @@ again:
 			return -1;
 		}
 
-		bistream->priv->read_length = strtoul (metabuf, NULL, 16);
+		/* ignore extensions */
+		boundary = strstr (metabuf, ";");
+		if (boundary)
+			*boundary = '\0';
+		else
+			metabuf[nread - 2] = '\0';
+
+		chunk_size = g_ascii_strtoull (metabuf, &end, 16);
+		if (*end) {
+			if (error && *error == NULL) {
+				g_set_error_literal (error, G_IO_ERROR,
+						     G_IO_ERROR_INVALID_ARGUMENT,
+						     _("Invalid chunk size"));
+			}
+			return -1;
+		}
+
+		if (chunk_size > G_MAXOFFSET) {
+			if (error && *error == NULL) {
+				g_set_error_literal (error, G_IO_ERROR,
+						     G_IO_ERROR_MESSAGE_TOO_LARGE,
+						     _("Too large chunk"));
+			}
+			return -1;
+		}
+		bistream->priv->read_length = (goffset)chunk_size;
 		if (bistream->priv->read_length > 0)
 			bistream->priv->chunked_state = SOUP_BODY_INPUT_STREAM_STATE_CHUNK;
 		else
diff --git a/libsoup/soup-message-io.c b/libsoup/soup-message-io.c
index c5412ab..7d448aa 100644
--- a/libsoup/soup-message-io.c
+++ b/libsoup/soup-message-io.c
@@ -781,8 +781,19 @@ io_read (SoupMessage *msg, gboolean blocking,
 		}
 
 		soup_buffer_free (buffer);
-		if (nread == -1)
-			return FALSE;
+		if (nread == -1) {
+			if (g_error_matches (*error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT) || g_error_matches (*error, G_IO_ERROR, G_IO_ERROR_PARTIAL_INPUT))
+				soup_message_set_status (msg, 400);
+			else if (g_error_matches (*error, G_IO_ERROR, G_IO_ERROR_MESSAGE_TOO_LARGE))
+				soup_message_set_status (msg, 413);
+			else
+				return FALSE;
+
+			g_clear_error (error);
+			soup_message_headers_append (msg->request_headers, "Connection", "close");
+			io->read_state = SOUP_MESSAGE_IO_STATE_FINISHING;
+			break;
+		}
 
 		/* else nread == 0 */
 		io->read_state = SOUP_MESSAGE_IO_STATE_BODY_DONE;
diff --git a/tests/server-test.c b/tests/server-test.c
index de52925..c79bcfb 100644
--- a/tests/server-test.c
+++ b/tests/server-test.c
@@ -1396,10 +1396,12 @@ do_chunked_test (ServerData *sd, gconstpointer test_data)
         struct {
                 const char *description;
                 const char *test;
+		const char *expected_response;
         } tests[] = {
-		{ "Single LF", "Transfer-Encoding: chunked\r\n\r\n5;ext\n data\r\n0\r\n\r\n" },
-		{ "Content-Length and Transfer-Encoding", "Content-Length: 4\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n" },
-		{ "Content-Length and Transfer-Encoding with keep alive connection", "Content-Length: 4\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\n\r\n0\r\n\r\n" },
+		{ "Single LF", "Transfer-Encoding: chunked\r\n\r\n5;ext\n data\r\n0\r\n\r\n", "HTTP/1.1 400 Bad Request" },
+		{ "Content-Length and Transfer-Encoding", "Content-Length: 4\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n", "HTTP/1.1 200 OK" },
+		{ "Content-Length and Transfer-Encoding with keep alive connection", "Content-Length: 4\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\n\r\n0\r\n\r\n", "HTTP/1.1 200 OK" },
+		{ "Request Entity Too Large", "Transfer-Encoding: chunked\r\nConnection: keep-alive\r\n\r\n8000000000000001\r\n\r\n\r\n", "HTTP/1.1 413 Request Entity Too Large" },
         };
 
         sd->server = soup_test_server_new (SOUP_TEST_SERVER_IN_THREAD);
@@ -1414,6 +1416,8 @@ do_chunked_test (ServerData *sd, gconstpointer test_data)
                 char *request;
                 char buffer[4096];
                 gssize nread;
+		GString *response;
+		const char *boundary;
                 GError *error = NULL;
 
                 debug_printf (1, "  %s\n", tests[i].description);
@@ -1429,11 +1433,22 @@ do_chunked_test (ServerData *sd, gconstpointer test_data)
                 g_output_stream_close (output, NULL, NULL);
                 g_socket_shutdown (g_socket_connection_get_socket (G_SOCKET_CONNECTION (conn)), FALSE, TRUE, &error);
 
+		response = g_string_new (NULL);
+
                 input = g_io_stream_get_input_stream (G_IO_STREAM (conn));
                 do {
-                        nread = g_input_stream_read (input, buffer, sizeof(buffer), NULL, NULL);
+			nread = g_input_stream_read (input, buffer, sizeof(buffer), NULL, &error);
+			g_assert_no_error (error);
+			if (nread >= 0)
+				response = g_string_append_len (response, (const char *)buffer, nread);
                 } while (nread > 0);
 
+		boundary = strstr (response->str, "\r\n");
+		g_assert_nonnull (boundary);
+		response = g_string_truncate (response, response->len - strlen (boundary));
+		g_assert_cmpstr (response->str, ==, tests[i].expected_response);
+		g_string_free (response, TRUE);
+
                 g_free (request);
                 g_object_unref (conn);
                 g_object_unref (client);
-- 
2.43.0