CVE-2018-25103: Insufficient Information (NVD-CWE-noinfo)
value: There exists use-after-free vulnerabilities in lighttpd <= 1.4.50 request parsing which might read from invalid pointers to memory used in the same request, not from other requests.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-06-17T18:15Z
Repository: https://github.com/lighttpd/lighttpd1.4
Language: C
Files changed: src/base.h, src/connections.c, src/http-header-glue.c, src/request.c, src/t/test_request.c, tests/request.t
Commit date: 2018-08-12 14:43:22 -0400
Commit message: [core] security: use-after-free invalid Range req

(thx Marcus Wengelin)

Diff: diff --git a/src/base.h b/src/base.h
index ea040f46..2fe60b6b 100644
--- a/src/base.h
+++ b/src/base.h
@@ -45,24 +45,20 @@ typedef struct {
 
 	buffer *request_line;
 
 	/* strings to the header */
 	buffer *http_host; /* not alloced */
-	const char   *http_range;
 	const char   *http_content_type;
 	const char   *http_if_modified_since;
 	const char   *http_if_none_match;
 
 	array  *headers;
 
 	/* CONTENT */
 	off_t content_length; /* returned by strtoll() */
 	off_t te_chunked;
 
-	/* internal representation */
-	int     accept_encoding;
-
 	/* internal */
 	buffer *pathinfo;
 } request;
 
 typedef struct {
diff --git a/src/connections.c b/src/connections.c
index 24365687..f06c89ea 100644
--- a/src/connections.c
+++ b/src/connections.c
@@ -683,11 +683,10 @@ int connection_reset(server *srv, connection *con) {
 
 #define CLEAN(x) \
 		con->request.x = NULL;
 
 	CLEAN(http_host);
-	CLEAN(http_range);
 	CLEAN(http_content_type);
 #undef CLEAN
 	con->request.content_length = 0;
 	con->request.te_chunked = 0;
 
diff --git a/src/http-header-glue.c b/src/http-header-glue.c
index 63057ee3..5d4f1f5f 100644
--- a/src/http-header-glue.c
+++ b/src/http-header-glue.c
@@ -227,11 +227,11 @@ int http_response_handle_cachable(server *srv, connection *con, buffer *mtime) {
 
 	return HANDLER_GO_ON;
 }
 
 
-static int http_response_parse_range(server *srv, connection *con, buffer *path, stat_cache_entry *sce) {
+static int http_response_parse_range(server *srv, connection *con, buffer *path, stat_cache_entry *sce, const char *range) {
 	int multipart = 0;
 	int error;
 	off_t start, end;
 	const char *s, *minus;
 	char *boundary = "fkj49sn38dcn3";
@@ -245,15 +245,30 @@ static int http_response_parse_range(server *srv, connection *con, buffer *path,
 
 	if (NULL != (ds = (data_string *)array_get_element(con->response.headers, "Content-Type"))) {
 		content_type = ds->value;
 	}
 
-	for (s = con->request.http_range, error = 0;
+	for (s = range, error = 0;
 	     !error && *s && NULL != (minus = strchr(s, '-')); ) {
 		char *err;
 		off_t la, le;
 
+		if (s != minus) {
+			la = strtoll(s, &err, 10);
+			if (err != minus) {
+				/* should not have multiple range-unit in Range, but
+				 * handle just in case multiple Range headers merged */
+				while (*s == ' ' || *s == '\t') ++s;
+				if (0 != strncmp(s, "bytes=", 6)) return -1;
+				s += 6;
+				if (s != minus) {
+					la = strtoll(s, &err, 10);
+					if (err != minus) return -1;
+				}
+			}
+		}
+
 		if (s == minus) {
 			/* -<stop> */
 
 			le = strtoll(s, &err, 10);
 
@@ -279,13 +294,10 @@ static int http_response_parse_range(server *srv, connection *con, buffer *path,
 			}
 
 		} else if (*(minus+1) == '\0' || *(minus+1) == ',') {
 			/* <start>- */
 
-			la = strtoll(s, &err, 10);
-
-			if (err == minus) {
 				/* ok */
 
 				if (*(err + 1) == '\0') {
 					s = err + 1;
 
@@ -299,20 +311,13 @@ static int http_response_parse_range(server *srv, connection *con, buffer *path,
 					end = sce->st.st_size - 1;
 					start = la;
 				} else {
 					error = 1;
 				}
-			} else {
-				/* error */
-				error = 1;
-			}
 		} else {
 			/* <start>-<stop> */
 
-			la = strtoll(s, &err, 10);
-
-			if (err == minus) {
 				le = strtoll(minus+1, &err, 10);
 
 				/* RFC 2616 - 14.35.1 */
 				if (la > le) {
 					error = 1;
@@ -333,15 +338,10 @@ static int http_response_parse_range(server *srv, connection *con, buffer *path,
 				} else {
 					/* error */
 
 					error = 1;
 				}
-			} else {
-				/* error */
-
-				error = 1;
-			}
 		}
 
 		if (!error) {
 			if (start < 0) start = 0;
 
@@ -514,13 +514,15 @@ void http_response_send_file (server *srv, connection *con, buffer *path) {
 		if (HANDLER_FINISHED == http_response_handle_cachable(srv, con, mtime)) {
 			return;
 		}
 	}
 
-	if (con->request.http_range && con->conf.range_requests
+	if (con->conf.range_requests
 	    && (200 == con->http_status || 0 == con->http_status)
+	    && NULL != (ds = (data_string *)array_get_element(con->request.headers, "Range"))
 	    && NULL == array_get_element(con->response.headers, "Content-Encoding")) {
+		buffer *range = ds->value;
 		int do_range_request = 1;
 		/* check if we have a conditional GET */
 
 		if (NULL != (ds = (data_string *)array_get_element(con->request.headers, "If-Range"))) {
 			/* if the value is the same as our ETag, we do a Range-request,
@@ -545,15 +547,18 @@ void http_response_send_file (server *srv, connection *con, buffer *path) {
 			} else if (!buffer_is_equal(ds->value, mtime)) {
 				do_range_request = 0;
 			}
 		}
 
-		if (do_range_request) {
+		if (do_range_request
+		    && !buffer_string_is_empty(range)
+		    && 0 == strncmp(range->ptr, "bytes=", 6)) {
+			/* support only "bytes" byte-unit */
 			/* content prepared, I'm done */
 			con->file_finished = 1;
 
-			if (0 == http_response_parse_range(srv, con, path, sce)) {
+			if (0 == http_response_parse_range(srv, con, path, sce, range->ptr+6)) {
 				con->http_status = 206;
 			}
 			return;
 		}
 	}
diff --git a/src/request.c b/src/request.c
index 4e7b1792..213a87e1 100644
--- a/src/request.c
+++ b/src/request.c
@@ -1116,34 +1116,10 @@ int http_request_parse(server *srv, connection *con) {
 									con->request.http_if_none_match = ds->value->ptr;
 								} else {
 									ds->free((data_unset*) ds);
 									ds = NULL;
 								}
-							} else if (cmp > 0 && 0 == (cmp = buffer_caseless_compare(CONST_BUF_LEN(ds->key), CONST_STR_LEN("Range")))) {
-								if (!con->request.http_range) {
-									/* bytes=.*-.* */
-
-									if (0 == strncasecmp(ds->value->ptr, "bytes=", 6) &&
-									    NULL != strchr(ds->value->ptr+6, '-')) {
-
-										/* if dup, only the first one will survive */
-										con->request.http_range = ds->value->ptr + 6;
-									}
-								} else {
-									con->http_status = 400;
-									con->keep_alive = 0;
-
-									if (srv->srvconf.log_request_header_on_error) {
-										log_error_write(srv, __FILE__, __LINE__, "s",
-												"duplicate Range-header -> 400");
-										log_error_write(srv, __FILE__, __LINE__, "Sb",
-												"request-header:\n",
-												con->request.request);
-									}
-									array_insert_unique(con->request.headers, (data_unset *)ds);
-									return 0;
-								}
 							}
 
 							if (ds) array_insert_unique(con->request.headers, (data_unset *)ds);
 						} else {
 							/* empty header-fields are not allowed by HTTP-RFC, we just ignore them */
diff --git a/src/t/test_request.c b/src/t/test_request.c
index 3b6671b4..c7734a0d 100644
--- a/src/t/test_request.c
+++ b/src/t/test_request.c
@@ -11,11 +11,10 @@
 static void test_request_connection_reset(connection *con)
 {
     con->request.http_method = HTTP_METHOD_UNSET;
     con->request.http_version = HTTP_VERSION_UNSET;
     con->request.http_host = NULL;
-    con->request.http_range = NULL;
     con->request.http_content_type = NULL;
     con->request.http_if_modified_since = NULL;
     con->request.http_if_none_match = NULL;
     con->request.content_length = 0;
     con->header_len = 0;
@@ -370,17 +369,44 @@ static void test_request_http_request_parse(server *srv, connection *con)
       CONST_STR_LEN("GET / HTTP/1.0\r\n"
                     "Content-Type: 5\r\n"
                     "Content-Type: 4\r\n"
                     "\r\n"));
 
-    run_http_request_parse(srv, con, __LINE__, 400,
-      "Duplicate Range headers",
+    /* (not actually testing Range here anymore; parsing deferred until use) */
+
+    run_http_request_parse(srv, con, __LINE__, 0,
+      "Duplicate Range headers (get appended)",
       CONST_STR_LEN("GET / HTTP/1.0\r\n"
                     "Range: bytes=5-6\r\n"
                     "Range: bytes=5-9\r\n"
                     "\r\n"));
 
+    run_http_request_parse(srv, con, __LINE__, 0,
+      "Duplicate Range headers with invalid range (a)",
+      CONST_STR_LEN("GET / HTTP/1.0\r\n"
+                    "Range: bytes=0\r\n"
+                    "Range: bytes=5-9\r\n"
+                    "\r\n"));
+    run_http_request_parse(srv, con, __LINE__, 0,
+      "Duplicate Range headers with invalid range (b)",
+      CONST_STR_LEN("GET / HTTP/1.0\r\n"
+                    "Range: bytes=5-9\r\n"
+                    "Range: bytes=0\r\n"
+                    "\r\n"));
+    run_http_request_parse(srv, con, __LINE__, 0,
+      "Duplicate Range headers with invalid range (c)",
+      CONST_STR_LEN("GET / HTTP/1.0\r\n"
+                    "Range: 0\r\n"
+                    "Range: bytes=5-9\r\n"
+                    "\r\n"));
+    run_http_request_parse(srv, con, __LINE__, 0,
+      "Duplicate Range headers with invalid range (d)",
+      CONST_STR_LEN("GET / HTTP/1.0\r\n"
+                    "Range: bytes=5-9\r\n"
+                    "Range: 0\r\n"
+                    "\r\n"));
+
     run_http_request_parse(srv, con, __LINE__, 0,
       "Duplicate If-None-Match headers",
       CONST_STR_LEN("GET / HTTP/1.0\r\n"
                     "If-None-Match: 5\r\n"
                     "If-None-Match: 4\r\n"
diff --git a/tests/request.t b/tests/request.t
index a9fe3f6a..c1e8f472 100755
--- a/tests/request.t
+++ b/tests/request.t
@@ -6,11 +6,11 @@ BEGIN {
 	unshift @INC, $srcdir;
 }
 
 use strict;
 use IO::Socket;
-use Test::More tests => 50;
+use Test::More tests => 52;
 use LightyTest;
 
 my $tf = LightyTest->new();
 my $t;
 
@@ -389,10 +389,30 @@ Range: bytes=0-
 EOF
  );
 $t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 200 } ];
 ok($tf->handle_http($t) == 0, 'GET, Range with range-requests-disabled');
 
+$t->{REQUEST}  = ( <<EOF
+GET /12345.txt HTTP/1.0
+Host: 123.example.org
+Range: 0
+Range: bytes=0-3
+EOF
+ );
+$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 200, 'HTTP-Content' => "12345\n" } ];
+ok($tf->handle_http($t) == 0, 'GET, Range invalid range-unit (first)');
+
+$t->{REQUEST}  = ( <<EOF
+GET /12345.txt HTTP/1.0
+Host: 123.example.org
+Range: bytes=0-3
+Range: 0
+EOF
+ );
+$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 206 } ];
+ok($tf->handle_http($t) == 0, 'GET, Range ignore invalid range (second)');
+
 $t->{REQUEST}  = ( <<EOF
 OPTIONS / HTTP/1.0
 Content-Length: 4
 
 1234

Vulnerable code: 	const char   *http_range;
	/* internal representation */
	int     accept_encoding;

	CLEAN(http_range);
static int http_response_parse_range(server *srv, connection *con, buffer *path, stat_cache_entry *sce) {
	for (s = con->request.http_range, error = 0;
			la = strtoll(s, &err, 10);

			if (err == minus) {
			} else {
				/* error */
				error = 1;
			}
			la = strtoll(s, &err, 10);

			if (err == minus) {
			} else {
				/* error */

				error = 1;
			}
	if (con->request.http_range && con->conf.range_requests
		if (do_range_request) {
			if (0 == http_response_parse_range(srv, con, path, sce)) {
							} else if (cmp > 0 && 0 == (cmp = buffer_caseless_compare(CONST_BUF_LEN(ds->key), CONST_STR_LEN("Range")))) {
								if (!con->request.http_range) {
									/* bytes=.*-.* */

									if (0 == strncasecmp(ds->value->ptr, "bytes=", 6) &&
									    NULL != strchr(ds->value->ptr+6, '-')) {

										/* if dup, only the first one will survive */
										con->request.http_range = ds->value->ptr + 6;
									}
								} else {
									con->http_status = 400;
									con->keep_alive = 0;

									if (srv->srvconf.log_request_header_on_error) {
										log_error_write(srv, __FILE__, __LINE__, "s",
												"duplicate Range-header -> 400");
										log_error_write(srv, __FILE__, __LINE__, "Sb",
												"request-header:\n",
												con->request.request);
									}
									array_insert_unique(con->request.headers, (data_unset *)ds);
									return 0;
								}
    con->request.http_range = NULL;
    run_http_request_parse(srv, con, __LINE__, 400,
      "Duplicate Range headers",
use Test::More tests => 50;
Fixed code: static int http_response_parse_range(server *srv, connection *con, buffer *path, stat_cache_entry *sce, const char *range) {
	for (s = range, error = 0;
		if (s != minus) {
			la = strtoll(s, &err, 10);
			if (err != minus) {
				/* should not have multiple range-unit in Range, but
				 * handle just in case multiple Range headers merged */
				while (*s == ' ' || *s == '\t') ++s;
				if (0 != strncmp(s, "bytes=", 6)) return -1;
				s += 6;
				if (s != minus) {
					la = strtoll(s, &err, 10);
					if (err != minus) return -1;
				}
			}
		}

	if (con->conf.range_requests
	    && NULL != (ds = (data_string *)array_get_element(con->request.headers, "Range"))
		buffer *range = ds->value;
		if (do_range_request
		    && !buffer_string_is_empty(range)
		    && 0 == strncmp(range->ptr, "bytes=", 6)) {
			/* support only "bytes" byte-unit */
			if (0 == http_response_parse_range(srv, con, path, sce, range->ptr+6)) {
    /* (not actually testing Range here anymore; parsing deferred until use) */

    run_http_request_parse(srv, con, __LINE__, 0,
      "Duplicate Range headers (get appended)",
    run_http_request_parse(srv, con, __LINE__, 0,
      "Duplicate Range headers with invalid range (a)",
      CONST_STR_LEN("GET / HTTP/1.0\r\n"
                    "Range: bytes=0\r\n"
                    "Range: bytes=5-9\r\n"
                    "\r\n"));
    run_http_request_parse(srv, con, __LINE__, 0,
      "Duplicate Range headers with invalid range (b)",
      CONST_STR_LEN("GET / HTTP/1.0\r\n"
                    "Range: bytes=5-9\r\n"
                    "Range: bytes=0\r\n"
                    "\r\n"));
    run_http_request_parse(srv, con, __LINE__, 0,
      "Duplicate Range headers with invalid range (c)",
      CONST_STR_LEN("GET / HTTP/1.0\r\n"
                    "Range: 0\r\n"
                    "Range: bytes=5-9\r\n"
                    "\r\n"));
    run_http_request_parse(srv, con, __LINE__, 0,
      "Duplicate Range headers with invalid range (d)",
      CONST_STR_LEN("GET / HTTP/1.0\r\n"
                    "Range: bytes=5-9\r\n"
                    "Range: 0\r\n"
                    "\r\n"));

use Test::More tests => 52;
$t->{REQUEST}  = ( <<EOF
GET /12345.txt HTTP/1.0
Host: 123.example.org
Range: 0
Range: bytes=0-3
EOF
 );
$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 200, 'HTTP-Content' => "12345\n" } ];
ok($tf->handle_http($t) == 0, 'GET, Range invalid range-unit (first)');

$t->{REQUEST}  = ( <<EOF
GET /12345.txt HTTP/1.0
Host: 123.example.org
Range: bytes=0-3
Range: 0
EOF
 );
$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 206 } ];
ok($tf->handle_http($t) == 0, 'GET, Range ignore invalid range (second)');