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(-)
@@ -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
@@ -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;
@@ -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