Skip to content

Commit 11422cc

Browse files
bugfix: rest: avoid segfault for invalid URI
`evhttp_uri_parse` can return a nullptr, for example when the URI contains invalid characters (e.g. "%"). `GetQueryParameterFromUri` passes the output of `evhttp_uri_parse` straight into `evhttp_uri_get_query`, which means that anyone calling a REST endpoint in which query parameters are used (e.g. `rest_headers`) can cause a segfault. This bugfix is designed to be minimal and without additional behaviour change. Follow-up work should be done to resolve this in a more general and robust way, so not every endpoint has to handle it individually.
1 parent 2bfe43d commit 11422cc

File tree

4 files changed

+33
-4
lines changed

4 files changed

+33
-4
lines changed

src/httpserver.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,9 @@ std::optional<std::string> HTTPRequest::GetQueryParameter(const std::string& key
673673
std::optional<std::string> GetQueryParameterFromUri(const char* uri, const std::string& key)
674674
{
675675
evhttp_uri* uri_parsed{evhttp_uri_parse(uri)};
676+
if (!uri_parsed) {
677+
throw std::runtime_error("URI parsing failed, it likely contained RFC 3986 invalid characters");
678+
}
676679
const char* query{evhttp_uri_get_query(uri_parsed)};
677680
std::optional<std::string> result;
678681

src/rest.cpp

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,11 @@ static bool rest_headers(const std::any& context,
200200
} else if (path.size() == 1) {
201201
// new path with query parameter: /rest/headers/<hash>?count=<count>
202202
hashStr = path[0];
203-
raw_count = req->GetQueryParameter("count").value_or("5");
203+
try {
204+
raw_count = req->GetQueryParameter("count").value_or("5");
205+
} catch (const std::runtime_error& e) {
206+
return RESTERR(req, HTTP_BAD_REQUEST, e.what());
207+
}
204208
} else {
205209
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid URI format. Expected /rest/headers/<hash>.<ext>?count=<count>");
206210
}
@@ -371,7 +375,11 @@ static bool rest_filter_header(const std::any& context, HTTPRequest* req, const
371375
} else if (uri_parts.size() == 2) {
372376
// new path with query parameter: /rest/blockfilterheaders/<filtertype>/<blockhash>?count=<count>
373377
raw_blockhash = uri_parts[1];
374-
raw_count = req->GetQueryParameter("count").value_or("5");
378+
try {
379+
raw_count = req->GetQueryParameter("count").value_or("5");
380+
} catch (const std::runtime_error& e) {
381+
return RESTERR(req, HTTP_BAD_REQUEST, e.what());
382+
}
375383
} else {
376384
return RESTERR(req, HTTP_BAD_REQUEST, "Invalid URI format. Expected /rest/blockfilterheaders/<filtertype>/<blockhash>.<ext>?count=<count>");
377385
}
@@ -652,11 +660,21 @@ static bool rest_mempool(const std::any& context, HTTPRequest* req, const std::s
652660
case RESTResponseFormat::JSON: {
653661
std::string str_json;
654662
if (param == "contents") {
655-
const std::string raw_verbose{req->GetQueryParameter("verbose").value_or("true")};
663+
std::string raw_verbose;
664+
try {
665+
raw_verbose = req->GetQueryParameter("verbose").value_or("true");
666+
} catch (const std::runtime_error& e) {
667+
return RESTERR(req, HTTP_BAD_REQUEST, e.what());
668+
}
656669
if (raw_verbose != "true" && raw_verbose != "false") {
657670
return RESTERR(req, HTTP_BAD_REQUEST, "The \"verbose\" query parameter must be either \"true\" or \"false\".");
658671
}
659-
const std::string raw_mempool_sequence{req->GetQueryParameter("mempool_sequence").value_or("false")};
672+
std::string raw_mempool_sequence;
673+
try {
674+
raw_mempool_sequence = req->GetQueryParameter("mempool_sequence").value_or("false");
675+
} catch (const std::runtime_error& e) {
676+
return RESTERR(req, HTTP_BAD_REQUEST, e.what());
677+
}
660678
if (raw_mempool_sequence != "true" && raw_mempool_sequence != "false") {
661679
return RESTERR(req, HTTP_BAD_REQUEST, "The \"mempool_sequence\" query parameter must be either \"true\" or \"false\".");
662680
}

src/test/httpserver_tests.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,9 @@ BOOST_AUTO_TEST_CASE(test_query_parameters)
3434
// Invalid query string syntax is the same as not having parameters
3535
uri = "/rest/endpoint/someresource.json&p1=v1&p2=v2";
3636
BOOST_CHECK(!GetQueryParameterFromUri(uri.c_str(), "p1").has_value());
37+
38+
// URI with invalid characters (%) raises a runtime error regardless of which query parameter is queried
39+
uri = "/rest/endpoint/someresource.json&p1=v1&p2=v2%";
40+
BOOST_CHECK_EXCEPTION(GetQueryParameterFromUri(uri.c_str(), "p1"), std::runtime_error, HasReason("URI parsing failed, it likely contained RFC 3986 invalid characters"));
3741
}
3842
BOOST_AUTO_TEST_SUITE_END()

test/functional/interface_rest.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,10 @@ def run_test(self):
277277
assert_equal(len(json_obj), 1) # ensure that there is one header in the json response
278278
assert_equal(json_obj[0]['hash'], bb_hash) # request/response hash should be the same
279279

280+
# Check invalid uri (% symbol at the end of the request)
281+
resp = self.test_rest_request(f"/headers/{bb_hash}%", ret_type=RetType.OBJ, status=400)
282+
assert_equal(resp.read().decode('utf-8').rstrip(), "URI parsing failed, it likely contained RFC 3986 invalid characters")
283+
280284
# Compare with normal RPC block response
281285
rpc_block_json = self.nodes[0].getblock(bb_hash)
282286
for key in ['hash', 'confirmations', 'height', 'version', 'merkleroot', 'time', 'nonce', 'bits', 'difficulty', 'chainwork', 'previousblockhash']:

0 commit comments

Comments
 (0)