This is an automated email from the ASF dual-hosted git repository.
liaoxin01 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 2137067cccd [fix](load) Reject malformed numeric HTTP headers instead
of killing BE (#68462)
2137067cccd is described below
commit 2137067cccd3d647ffa36e6363e2365856864838
Author: Xin Liao <[email protected]>
AuthorDate: Wed Oct 14 22:51:57 2026 +0800
[fix](load) Reject malformed numeric HTTP headers instead of killing BE
(#68462)
### What problem does this PR solve?
Problem Summary:
A single raw request to the BE HTTP port takes the whole BE process
down:
PUT /api/_http_stream HTTP/1.1
Host: <be>
group_commit: async_mode
Content-Length: not_a_number
The client gets no response and times out, FE then fails queries with
`No backend available as scan node`, and `SHOW BACKENDS` reports the BE
as
not alive until it is restarted.
Root cause: `HttpStreamAction::_handle_group_commit()` parsed
`Content-Length`
with a bare `std::stoll()`. libevent invokes the header hook from its
own C
frames, so the `std::invalid_argument` thrown there unwinds through C
code and
terminates the process. `StreamLoadAction::_can_group_commit()` already
guards
the same parse with try/catch - the http stream path was simply missed.
Auditing the other numeric parses behind the BE http port, three more
are
reachable from untrusted input and unguarded, all the same one line
defect:
- `wal_id` in `HttpStreamAction::on_chunk_data()`
- the deprecated `auth_code` in `parse_basic_auth()`, which every load
action calls
- `reset_value` in `SetJeHeapProfileResetActions::handle()`, i.e. a
plain
`GET /jeheap/reset/not_a_number`
Everything else already catches, so this closes the class.
The fix adds `safe_stoll()` next to the existing `safe_stoi()` in
`string_util`
and parses all four values with it, so a malformed value fails that one
request
instead of the process:
- `Content-Length` and `wal_id` fail the load with `INVALID_ARGUMENT`
- a malformed `auth_code` is simply an invalid credential, so
authentication
fails. The value itself is kept out of the log, matching the masking
that
`is_sensitive_header()` already applies to that header
- `reset_value` answers `400`
One caller needed a fix to make the `auth_code` rejection effective:
`StreamLoad2PCAction::handle()` built the authentication error and then
overwrote it with the result of `operate_txn_2pc()`, so the request
reached FE
anyway. It now answers with the error and returns, like the other
failure paths
in that same function.
After the fix the request above is answered with an `INVALID_ARGUMENT`
status
and the BE stays alive.
---
be/src/service/http/action/http_stream.cpp | 18 ++++++++----
be/src/service/http/action/jeprofile_actions.cpp | 12 ++++++--
be/src/service/http/action/stream_load_2pc.cpp | 3 ++
be/src/service/http/utils.cpp | 11 +++++++-
be/src/util/string_util.cpp | 12 ++++++++
be/src/util/string_util.h | 2 ++
be/test/service/http/http_utils_test.cpp | 22 +++++++++++++++
be/test/service/http/stream_load_test.cpp | 36 ++++++++++++++++++++++++
be/test/util/string_util_test.cpp | 15 ++++++++++
9 files changed, 123 insertions(+), 8 deletions(-)
diff --git a/be/src/service/http/action/http_stream.cpp
b/be/src/service/http/action/http_stream.cpp
index c374b21de7d..0da3aacca8c 100644
--- a/be/src/service/http/action/http_stream.cpp
+++ b/be/src/service/http/action/http_stream.cpp
@@ -268,8 +268,14 @@ void HttpStreamAction::on_chunk_data(HttpRequest* req) {
if (ctx == nullptr || !ctx->status.ok()) {
return;
}
- if (!req->header(HTTP_WAL_ID_KY).empty()) {
- ctx->wal_id = std::stoll(req->header(HTTP_WAL_ID_KY));
+ const auto& wal_id_str = req->header(HTTP_WAL_ID_KY);
+ if (!wal_id_str.empty()) {
+ auto wal_id = safe_stoll(wal_id_str, HTTP_WAL_ID_KY);
+ if (!wal_id.has_value()) {
+ ctx->status = std::move(wal_id).error();
+ return;
+ }
+ ctx->wal_id = wal_id.value();
}
struct evhttp_request* ev_req = req->get_evhttp_request();
auto evbuf = evhttp_request_get_input_buffer(ev_req);
@@ -447,9 +453,11 @@ Status HttpStreamAction::_handle_group_commit(HttpRequest*
req,
if (config::wait_internal_group_commit_finish) {
group_commit_mode = "sync_mode";
}
- int64_t content_length = req->header(HttpHeaders::CONTENT_LENGTH).empty()
- ? 0
- :
std::stoll(req->header(HttpHeaders::CONTENT_LENGTH));
+ int64_t content_length = 0;
+ const auto& content_length_str = req->header(HttpHeaders::CONTENT_LENGTH);
+ if (!content_length_str.empty()) {
+ content_length = DORIS_TRY(safe_stoll(content_length_str,
HttpHeaders::CONTENT_LENGTH));
+ }
if (content_length < 0) {
std::stringstream ss;
ss << "This http load content length <0 (" << content_length
diff --git a/be/src/service/http/action/jeprofile_actions.cpp
b/be/src/service/http/action/jeprofile_actions.cpp
index d7702c8fb63..3ab041bd21d 100644
--- a/be/src/service/http/action/jeprofile_actions.cpp
+++ b/be/src/service/http/action/jeprofile_actions.cpp
@@ -20,6 +20,7 @@
#include <jemalloc/jemalloc.h>
#include <unistd.h>
+#include <cstdint>
#include <string>
#include "agent/utils.h"
@@ -31,6 +32,7 @@
#include "service/http/http_handler_with_auth.h"
#include "service/http/http_headers.h"
#include "service/http/http_request.h"
+#include "util/string_util.h"
namespace doris {
@@ -92,9 +94,15 @@ void SetJeHeapProfileResetActions::handle(HttpRequest* req) {
req->add_output_header(HttpHeaders::CONTENT_TYPE, HEADER_JSON.c_str());
if (compile_check(req)) {
const auto& lg_sample_str = req->param("reset_value");
- size_t lg_sample = std::stol(lg_sample_str);
+ auto parsed_lg_sample = safe_stoll(lg_sample_str, "reset_value");
+ if (!parsed_lg_sample.has_value()) {
+ HttpChannel::send_reply(req, HttpStatus::BAD_REQUEST,
+
parsed_lg_sample.error().to_string_no_stack() + "\n");
+ return;
+ }
+ int64_t lg_sample = parsed_lg_sample.value();
if (lg_sample > 0 &&
HeapProfiler::instance()->check_enable_heap_profiler() &&
- HeapProfiler::instance()->heap_profiler_reset(lg_sample)) {
+
HeapProfiler::instance()->heap_profiler_reset(static_cast<size_t>(lg_sample))) {
HttpChannel::send_reply(req, HttpStatus::OK,
fmt::format("Jemalloc reset all memory
profile statistics and "
"update the sample rate:{}\n",
diff --git a/be/src/service/http/action/stream_load_2pc.cpp
b/be/src/service/http/action/stream_load_2pc.cpp
index 15ca8066cf9..4a20b0f8b93 100644
--- a/be/src/service/http/action/stream_load_2pc.cpp
+++ b/be/src/service/http/action/stream_load_2pc.cpp
@@ -82,6 +82,9 @@ void StreamLoad2PCAction::handle(HttpRequest* req) {
if (!parse_basic_auth(*req, &ctx->auth)) {
LOG(WARNING) << "parse basic authorization failed.";
status = Status::InternalError("no valid Basic authorization");
+ status_result = status.to_json();
+ HttpChannel::send_reply(req, HttpStatus::OK, status_result);
+ return;
}
status = _exec_env->stream_load_executor()->operate_txn_2pc(ctx.get());
diff --git a/be/src/service/http/utils.cpp b/be/src/service/http/utils.cpp
index a3afe6e8a30..38de663d010 100644
--- a/be/src/service/http/utils.cpp
+++ b/be/src/service/http/utils.cpp
@@ -46,6 +46,7 @@
#include "util/md5.h"
#include "util/path_util.h"
#include "util/security.h"
+#include "util/string_util.h"
#include "util/url_coding.h"
namespace doris {
@@ -111,7 +112,15 @@ bool parse_basic_auth(const HttpRequest& req, AuthInfo*
auth) {
} else if (!auth_token.empty()) {
auth->token = auth_token;
} else if (!auth_code.empty()) {
- auth->auth_code = std::stoll(auth_code); // deprecated
+ // auth_code comes straight from a request header, a malformed one is
just an
+ // invalid credential and must not throw out of the http callback. The
value is a
+ // credential that is_sensitive_header() masks elsewhere, so it is not
logged.
+ auto parsed_auth_code = safe_stoll(auth_code, HTTP_AUTH_CODE);
+ if (!parsed_auth_code.has_value()) {
+ LOG(WARNING) << "parse auth code failed, malformed " <<
HTTP_AUTH_CODE << " header";
+ return false;
+ }
+ auth->auth_code = parsed_auth_code.value(); // deprecated
} else if (!valid_basic_auth) {
return false;
}
diff --git a/be/src/util/string_util.cpp b/be/src/util/string_util.cpp
index 259392b020d..0588a78aaa3 100644
--- a/be/src/util/string_util.cpp
+++ b/be/src/util/string_util.cpp
@@ -43,4 +43,16 @@ Result<int> safe_stoi(const std::string& input, const
std::string& name) {
std::string("'{}' value out of range: '{}', {}"), name, input,
e.what()));
}
}
+
+Result<int64_t> safe_stoll(const std::string& input, const std::string& name) {
+ try {
+ return std::stoll(input);
+ } catch (const std::invalid_argument& e) {
+ return ResultError(Status::Error<ErrorCode::INVALID_ARGUMENT>(
+ std::string("Invalid format of '{}': '{}', {}"), name, input,
e.what()));
+ } catch (const std::out_of_range& e) {
+ return ResultError(Status::Error<ErrorCode::INVALID_ARGUMENT>(
+ std::string("'{}' value out of range: '{}', {}"), name, input,
e.what()));
+ }
+}
} // namespace doris
diff --git a/be/src/util/string_util.h b/be/src/util/string_util.h
index 54a93fff8c6..6cf7f7c2956 100644
--- a/be/src/util/string_util.h
+++ b/be/src/util/string_util.h
@@ -24,6 +24,7 @@
#include <boost/tokenizer.hpp>
#include <cctype>
#include <cstddef>
+#include <cstdint>
#include <map>
#include <set>
#include <sstream>
@@ -143,6 +144,7 @@ public:
size_t hash_of_path(const std::string& identifier, const std::string& path);
Result<int> safe_stoi(const std::string& input, const std::string& name);
+Result<int64_t> safe_stoll(const std::string& input, const std::string& name);
using StringCaseSet = std::set<std::string, StringCaseLess>;
using StringCaseUnorderedSet = std::unordered_set<std::string,
StringCaseHasher, StringCaseEqual>;
template <class T>
diff --git a/be/test/service/http/http_utils_test.cpp
b/be/test/service/http/http_utils_test.cpp
index ba096bc153f..9f7942043a4 100644
--- a/be/test/service/http/http_utils_test.cpp
+++ b/be/test/service/http/http_utils_test.cpp
@@ -21,7 +21,9 @@
#include <string>
+#include "common/utils.h"
#include "gtest/gtest_pred_impl.h"
+#include "service/http/http_common.h"
#include "service/http/http_headers.h"
#include "service/http/http_request.h"
#include "service/http/utils.h"
@@ -96,4 +98,24 @@ TEST_F(HttpUtilsTest, parse_basic_auth) {
}
}
+TEST_F(HttpUtilsTest, parse_auth_code) {
+ {
+ HttpRequest req(_evhttp_req);
+ req._headers.emplace(HttpHeaders::AUTHORIZATION,
encode_basic_auth("doris", "passwd"));
+ req._headers.emplace(HTTP_AUTH_CODE, "1234567890123");
+ AuthInfo auth;
+ EXPECT_TRUE(parse_basic_auth(req, &auth));
+ EXPECT_EQ(1234567890123, auth.auth_code);
+ }
+ {
+ // a malformed auth code is rejected instead of throwing out of the
http callback
+ HttpRequest req(_evhttp_req);
+ req._headers.emplace(HttpHeaders::AUTHORIZATION,
encode_basic_auth("doris", "passwd"));
+ req._headers.emplace(HTTP_AUTH_CODE, "not_a_number");
+ AuthInfo auth;
+ EXPECT_FALSE(parse_basic_auth(req, &auth));
+ EXPECT_EQ(-1, auth.auth_code);
+ }
+}
+
} // namespace doris
diff --git a/be/test/service/http/stream_load_test.cpp
b/be/test/service/http/stream_load_test.cpp
index 93dd9d044c7..1405092f401 100644
--- a/be/test/service/http/stream_load_test.cpp
+++ b/be/test/service/http/stream_load_test.cpp
@@ -195,4 +195,40 @@ TEST_F(StreamLoadTest,
HttpStreamBodySizeLimitPlusOneErrorIncludesExactBytes) {
"this load is reasonable");
evhttp_request_free(evhttp_req);
}
+
+// A malformed Content-Length used to escape as an exception from the libevent
header
+// callback and take the whole BE process down.
+TEST_F(StreamLoadTest, HttpStreamMalformedContentLengthIsRejected) {
+ for (const auto* content_length : {"not_a_number", "9223372036854775808"})
{
+ auto* evhttp_req = evhttp_request_new(nullptr, nullptr);
+ HttpRequest req(evhttp_req);
+ req.set_header(HTTP_GROUP_COMMIT, "async_mode");
+ req.set_header(HttpHeaders::CONTENT_LENGTH, content_length);
+
+ HttpStreamAction action(nullptr);
+ auto ctx = std::make_shared<StreamLoadContext>(nullptr);
+ auto status = action._handle_group_commit(&req, ctx);
+
+ EXPECT_TRUE(status.is<ErrorCode::INVALID_ARGUMENT>()) <<
content_length;
+ evhttp_request_free(evhttp_req);
+ }
+}
+
+TEST_F(StreamLoadTest, HttpStreamMalformedWalIdIsRejected) {
+ auto* evhttp_req = evhttp_request_new(nullptr, nullptr);
+ HttpRequest req(evhttp_req);
+ req.set_header(HTTP_WAL_ID_KY, "not_a_number");
+
+ HttpStreamAction action(nullptr);
+ auto ctx = std::make_shared<StreamLoadContext>(nullptr);
+ req.set_handler(&action);
+ req.set_handler_ctx(ctx);
+
+ action.on_chunk_data(&req);
+
+ EXPECT_TRUE(ctx->status.is<ErrorCode::INVALID_ARGUMENT>());
+ // ctx carries no ExecEnv, so detach it before ~HttpRequest calls
free_handler_ctx()
+ req.set_handler_ctx(nullptr);
+ evhttp_request_free(evhttp_req);
+}
} // namespace doris
diff --git a/be/test/util/string_util_test.cpp
b/be/test/util/string_util_test.cpp
index 77b090be4c2..8a8bb3a92da 100644
--- a/be/test/util/string_util_test.cpp
+++ b/be/test/util/string_util_test.cpp
@@ -200,4 +200,19 @@ TEST_F(StringUtilTest, normal) {
}
}
+TEST_F(StringUtilTest, safe_stoll) {
+ EXPECT_EQ(0, safe_stoll("0", "content-length").value());
+ EXPECT_EQ(-1, safe_stoll("-1", "content-length").value());
+ // beyond the range of safe_stoi
+ EXPECT_EQ(9223372036854775807LL, safe_stoll("9223372036854775807",
"content-length").value());
+
+ auto invalid = safe_stoll("not_a_number", "content-length");
+ EXPECT_FALSE(invalid.has_value());
+ EXPECT_TRUE(invalid.error().is<ErrorCode::INVALID_ARGUMENT>());
+
+ auto out_of_range = safe_stoll("9223372036854775808", "content-length");
+ EXPECT_FALSE(out_of_range.has_value());
+ EXPECT_TRUE(out_of_range.error().is<ErrorCode::INVALID_ARGUMENT>());
+}
+
} // namespace doris
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]