From e280a162e43e7106e6f3ae4fdd0c9e3833d45def Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Oto=20=C5=A0=C5=A5=C3=A1va?= <oto.stava@nic.cz>
Date: Fri, 17 Jun 2022 10:57:58 +0200
Subject: [PATCH] daemon/http: improve URI checks

The `check_uri()` function now only checks that the endpoint is either
`/doh` or `/dns-query`. Parameter checks were moved into
`process_uri_path()` so that the check only takes place for GET
requests. POST requests now do not care about parameters at all.
---
 NEWS                       |  8 +++++
 daemon/http.c              | 72 +++++++++++++-------------------------
 tests/config/doh2.test.lua | 51 +++++++++++++++++++++++++++
 3 files changed, 83 insertions(+), 48 deletions(-)

diff --git a/NEWS b/NEWS
index babcb287a..19499650d 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,11 @@
+Knot Resolver 5.5.2 (2022-mm-dd)
+================================
+
+Bugfixes
+--------
+- daemon/http: improved URI checks to fix some proxies (#746, !1311)
+
+
 Knot Resolver 5.5.1 (2022-06-14)
 ================================
 
diff --git a/daemon/http.c b/daemon/http.c
index 2e8aa2f00..3347aa6f4 100644
--- a/daemon/http.c
+++ b/daemon/http.c
@@ -172,25 +172,14 @@ static int send_data_callback(nghttp2_session *h2, nghttp2_frame *frame, const u
 /*
  * Check endpoint and uri path
  */
-static int check_uri(const char* uri_path)
+static int check_uri(const char* path)
 {
-	static const char key[] = "dns=";
-	static const char *delim = "&";
 	static const char *endpoints[] = {"dns-query", "doh"};
-	char *beg;
-	char *end_prev;
 	ssize_t endpoint_len;
 	ssize_t ret;
 
-	if (!uri_path)
-		return kr_error(EINVAL);
-
-	auto_free char *path = malloc(sizeof(*path) * (strlen(uri_path) + 1));
 	if (!path)
-		return kr_error(ENOMEM);
-
-	memcpy(path, uri_path, strlen(uri_path));
-	path[strlen(uri_path)] = '\0';
+		return kr_error(EINVAL);
 
 	char *query_mark = strstr(path, "?");
 
@@ -208,35 +197,7 @@ static int check_uri(const char* uri_path)
 			break;
 	}
 
-	if (ret) /* no endpoint found */
-		return kr_error(ENOENT);
-
-	/* FIXME This also passes for GET when no variables are provided.
-	 * Fixing it doesn't seem straightforward, since :method may not be
-	 * known by the time check_uri() is called... */
-	if (endpoint_len == strlen(path) - 1) /* done for POST method */
-		return 0;
-
-	/* go over key:value pair */
-	beg = strtok(query_mark + 1, delim);
-	if (beg) {
-		while (beg != NULL) {
-			if (!strncmp(beg, key, 4)) { /* dns variable in path found */
-				break;
-			}
-			end_prev = beg + strlen(beg);
-			beg = strtok(NULL, delim);
-			if (!beg || beg-1 != end_prev) { /* detect && */
-				return -1;
-			}
-		}
-
-		if (!beg) { /* no dns variable in path */
-			return -1;
-		}
-	}
-
-	return 0;
+	return (ret) ? kr_error(ENOENT) : kr_ok();
 }
 
 static kr_http_header_array_t *headers_dup(kr_http_header_array_t *src)
@@ -265,13 +226,26 @@ static int process_uri_path(struct http_ctx *ctx, const char* path, int32_t stre
 		return kr_error(EINVAL);
 
 	static const char key[] = "dns=";
-	char *beg = strstr(path, key);
-	char *end;
-	size_t remaining;
+	static const char *delim = "&";
+	char *beg, *end;
 	uint8_t *dest;
+	uint32_t remaining;
 
-	if (!beg)  /* No dns variable in path. */
-		return -1;
+	if (!path)
+		return kr_error(EINVAL);
+
+	char *query_mark = strstr(path, "?");
+	if (!query_mark || strlen(query_mark) == 0) /* no parameters in path */
+		return kr_error(EINVAL);
+
+	/* go over key:value pair */
+	for (beg = strtok(query_mark + 1, delim); beg != NULL; beg = strtok(NULL, delim)) {
+		if (!strncmp(beg, key, 4)) /* dns variable in path found */
+			break;
+	}
+
+	if (!beg) /* no dns variable in path */
+		return kr_error(EINVAL);
 
 	beg += sizeof(key) - 1;
 	end = strchr(beg, '&');
@@ -282,6 +256,7 @@ static int process_uri_path(struct http_ctx *ctx, const char* path, int32_t stre
 	remaining = ctx->buf_size - ctx->submitted - ctx->buf_pos;
 	dest = ctx->buf + ctx->buf_pos;
 
+	/* Decode dns message from the parameter */
 	int ret = kr_base64url_decode((uint8_t*)beg, end - beg, dest, remaining);
 	if (ret < 0) {
 		ctx->buf_pos = 0;
@@ -296,7 +271,8 @@ static int process_uri_path(struct http_ctx *ctx, const char* path, int32_t stre
 		.headers = headers_dup(ctx->headers)
 	};
 	queue_push(ctx->streams, stream);
-	return 0;
+
+	return kr_ok();
 }
 
 static void refuse_stream(nghttp2_session *h2, int32_t stream_id)
diff --git a/tests/config/doh2.test.lua b/tests/config/doh2.test.lua
index 881515c7b..2360e7f48 100644
--- a/tests/config/doh2.test.lua
+++ b/tests/config/doh2.test.lua
@@ -139,6 +139,45 @@ else
 		same(pkt:arcount(), 0, desc .. ': ADDITIONAL is empty')
 	end
 
+	local function test_post_ignore_params_1()
+		local desc = 'valid POST ignores parameters (without ?dns)'
+		local req = req_templ:clone()
+		req.headers:upsert(':method', 'POST')
+		req.headers:upsert(':path', '/doh?something=asdf&aaa=bbb')
+		req:set_body(basexx.from_base64(  -- noerror.test. A
+			'vMEBAAABAAAAAAAAB25vZXJyb3IEdGVzdAAAAQAB'))
+		local headers, pkt = check_ok(req, desc)
+		if not (headers and pkt) then
+			return
+		end
+		-- HTTP TTL is minimum from all RRs in the answer
+		same(headers:get('cache-control'), 'max-age=300', desc .. ': TTL 900')
+		same(pkt:rcode(), kres.rcode.NOERROR, desc .. ': rcode matches')
+		same(pkt:ancount(), 3, desc .. ': ANSWER is present')
+		same(pkt:nscount(), 1, desc .. ': AUTHORITY is present')
+		same(pkt:arcount(), 0, desc .. ': ADDITIONAL is empty')
+	end
+
+	local function test_post_ignore_params_2()
+		local desc = 'valid POST ignores parameters (with ?dns)'
+		local req = req_templ:clone()
+		req.headers:upsert(':method', 'POST')
+		req.headers:upsert(':path', '/dns-query?dns='  -- servfail.test. A
+			.. 'FZUBAAABAAAAAAAACHNlcnZmYWlsBHRlc3QAAAEAAQ')
+		req:set_body(basexx.from_base64(  -- noerror.test. A
+			'vMEBAAABAAAAAAAAB25vZXJyb3IEdGVzdAAAAQAB'))
+		local headers, pkt = check_ok(req, desc)
+		if not (headers and pkt) then
+			return
+		end
+		-- HTTP TTL is minimum from all RRs in the answer
+		same(headers:get('cache-control'), 'max-age=300', desc .. ': TTL 900')
+		same(pkt:rcode(), kres.rcode.NOERROR, desc .. ': rcode matches')
+		same(pkt:ancount(), 3, desc .. ': ANSWER is present')
+		same(pkt:nscount(), 1, desc .. ': AUTHORITY is present')
+		same(pkt:arcount(), 0, desc .. ': ADDITIONAL is empty')
+	end
+
 	local function test_post_nxdomain()
 		local desc = 'valid POST query which ends with NXDOMAIN'
 		local req = req_templ:clone()
@@ -252,6 +291,15 @@ else
 		same(pkt:nscount(), 1, desc .. ': AUTHORITY is present')
 	end
 
+	local function test_get_multiple_amps()
+		local desc = 'GET query with consecutive ampersands'
+		local req = req_templ:clone()
+		req.headers:upsert(':method', 'GET')
+		req.headers:upsert(':path',
+		'/doh?other=something&another=something&&&&dns=vMEBAAABAAAAAAAAB25vZXJyb3IEdGVzdAAAAQAB')
+		check_ok(req, desc)
+	end
+
 	local function test_get_other_params_before_dns()
 		local desc = 'GET query with other parameters before dns is valid'
 		local req = req_templ:clone()
@@ -446,6 +494,8 @@ else
 		start_server,
 		test_post_servfail,
 		test_post_noerror,
+		test_post_ignore_params_1,
+		test_post_ignore_params_2,
 		test_post_nxdomain,
 		test_huge_answer,
 		test_post_short_input,
@@ -455,6 +505,7 @@ else
 		test_get_servfail,
 		test_get_noerror,
 		test_get_nxdomain,
+		test_get_multiple_amps,
 		test_get_other_params_before_dns,
 		test_get_other_params_after_dns,
 		test_get_other_params,
-- 
GitLab