From 00dfe21e30d370c1601f9cd70eb2ce85d57db912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Kugland?= Date: Fri, 15 May 2026 22:15:28 -0300 Subject: [PATCH 1/3] test: reject requests with malformed request-line Add test cases for two forms of invalid request-line: - a literal space in the request-target (e.g. GET /foo bar HTTP/1.1), which is invalid per RFC 9112 and should return 400 Bad Request; - extra tokens after the HTTP version (e.g. GET / HTTP/1.1 HTTP/1.1), which should likewise be rejected. --- devel/test.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/devel/test.py b/devel/test.py index 4e12f69..520fa97 100755 --- a/devel/test.py +++ b/devel/test.py @@ -255,6 +255,7 @@ def setUpModule(): ["invalid up dir", "/../", "assertIsInvalid"], ["fancy invalid up dir", "/./dir/./../../", "assertIsInvalid"], ["ascii nul", "/\x00/", "assertBadRequest"], + ["space in url", "/foo bar", "assertBadRequest"], ["extra slashes 2", "//.d", "assertNotFound"], ["not found", "/not_found.txt", "assertNotFound"], ["not found dir", "/not_found/", "assertNotFound"], @@ -263,6 +264,25 @@ def setUpModule(): ]: makeSimpleCases(*args) +class TestMalformedRequestLine(TestHelper): + def _raw(self, request_line): + c = Conn() + c.s.send((request_line + "\r\n\r\n").encode("utf-8")) + resp = b"" + while True: + signal.alarm(1) + r = c.s.recv(65536) + signal.alarm(0) + if not r: + break + resp += r + c.close() + return resp + + def test_extra_token_after_version(self): + resp = self._raw("GET / HTTP/1.1 HTTP/1.1\r\nConnection: close") + self.assertBadRequest(resp, "/") + class TestDirRedirect(TestHelper): def setUp(self): self.url = "/mydir" From 99a9c25ed1aac1603a39c767e9962539c92d2351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Kugland?= Date: Fri, 15 May 2026 22:15:37 -0300 Subject: [PATCH 2/3] fix: reject requests with malformed request-line In parse_request(), after parsing the HTTP version token, reject the request if: - the token is not HTTP/1.0 or HTTP/1.1 (catches literal spaces in the request-target, which cause the URL parser to stop early and treat the next word as the version); - a non-whitespace character follows the version on the same line (catches extra tokens such as GET / HTTP/1.1 HTTP/1.1). Also stop the version-token loop at '\n' so bare-LF requests are handled correctly, and force conn_close=1 in process_request() when parse_request() fails so the 400 reply always closes the connection. --- darkhttpd.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/darkhttpd.c b/darkhttpd.c index bcfb248..d97ce8a 100644 --- a/darkhttpd.c +++ b/darkhttpd.c @@ -1944,14 +1944,21 @@ static int parse_request(struct connection *conn) { for (bound2 = bound1 + 1; (bound2 < conn->request_length) && (conn->request[bound2] != ' ') && - (conn->request[bound2] != '\r'); + (conn->request[bound2] != '\r') && + (conn->request[bound2] != '\n'); bound2++) ; proto = split_string(conn->request, bound1, bound2); if (strcasecmp(proto, "HTTP/1.1") == 0) conn->conn_close = 0; + else if (strcasecmp(proto, "HTTP/1.0") != 0) { + free(proto); + return 0; /* unknown version or literal space in request-target */ + } free(proto); + if (conn->request[bound2] != '\r' && conn->request[bound2] != '\n') + return 0; /* extra tokens after HTTP version */ } /* parse connection field */ @@ -2519,6 +2526,7 @@ static void process_request(struct connection *conn) { num_requests++; if (!parse_request(conn)) { + conn->conn_close = 1; default_reply(conn, 400, "Bad Request", "You sent a request that the server couldn't understand."); } From f7fe2866e59a52e204538ac0f7d86cd6d46e4c32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Kugland?= Date: Mon, 18 May 2026 14:28:45 -0300 Subject: [PATCH 3/3] refactor: check for extra tokens before allocating proto string Avoids a malloc/free in the reject path when extra tokens follow the HTTP version on the request line, as suggested in code review. --- darkhttpd.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/darkhttpd.c b/darkhttpd.c index d97ce8a..3d6a4d3 100644 --- a/darkhttpd.c +++ b/darkhttpd.c @@ -1949,6 +1949,8 @@ static int parse_request(struct connection *conn) { bound2++) ; + if (conn->request[bound2] != '\r' && conn->request[bound2] != '\n') + return 0; /* extra tokens after HTTP version */ proto = split_string(conn->request, bound1, bound2); if (strcasecmp(proto, "HTTP/1.1") == 0) conn->conn_close = 0; @@ -1957,8 +1959,6 @@ static int parse_request(struct connection *conn) { return 0; /* unknown version or literal space in request-target */ } free(proto); - if (conn->request[bound2] != '\r' && conn->request[bound2] != '\n') - return 0; /* extra tokens after HTTP version */ } /* parse connection field */