This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git
commit 51d4b709b7a09549b70997e7e001060a927cacf5
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Thu Oct 15 09:29:19 2026 +0800
branch-4.1: [fix](load) Reject malformed numeric HTTP headers instead of
killing BE #68462 (#69081)
Cherry-picked from #68462
Co-authored-by: Xin Liao <[email protected]>
---
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 92cdf6b9c0b..9a17dfa218b 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 e8c36b00cf1..862f5f07d73 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"
@@ -30,6 +31,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 746eabff165..8f49505981c 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]