This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.1 by this push:
new e0cfca1d075 branch-4.1: [improvement](log) Log the user name of HTTP
requests for auditing #66745 (#67319)
e0cfca1d075 is described below
commit e0cfca1d0750c93870f9476ef78e87cedb43166b
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Mon Sep 7 09:17:52 2026 +0800
branch-4.1: [improvement](log) Log the user name of HTTP requests for
auditing #66745 (#67319)
Cherry-picked from #66745
Co-authored-by: Xin Liao <[email protected]>
---
be/src/service/http/http_request.cpp | 32 ++++-
be/test/service/http/http_request_test.cpp | 142 +++++++++++++++++++++
.../doris/httpv2/controller/BaseController.java | 7 +-
3 files changed, 178 insertions(+), 3 deletions(-)
diff --git a/be/src/service/http/http_request.cpp
b/be/src/service/http/http_request.cpp
index 36d5a87a77a..b5c6ae2e48f 100644
--- a/be/src/service/http/http_request.cpp
+++ b/be/src/service/http/http_request.cpp
@@ -31,6 +31,7 @@
#include "service/http/http_handler.h"
#include "service/http/http_headers.h"
#include "util/stack_util.h"
+#include "util/url_coding.h"
namespace doris {
@@ -43,6 +44,34 @@ static bool is_sensitive_header(const std::string&
header_name) {
iequal(header_name, HttpHeaders::AUTH_TOKEN) || iequal(header_name,
"auth_code");
}
+// Renders a sensitive header for logging. For HTTP Basic credentials the user
name is kept,
+// so that logs still answer "who issued this request", and only the password
is masked,
+// yielding "<user>:***MASKED***". Any other sensitive header, and any
credential we fail to
+// parse, is masked as a whole. The result is a rendering, not the header
value: the real one
+// is base64 encoded.
+static std::string mask_sensitive_header(const std::string& name, const
std::string& value) {
+ static const std::string kMasked = "***MASKED***";
+ if (!iequal(name, HttpHeaders::AUTHORIZATION)) {
+ return kMasked;
+ }
+
+ // Expected form: "Basic <base64(user:password)>"
+ auto pos = value.find(' ');
+ if (pos == std::string::npos || !iequal(value.substr(0, pos), "Basic")) {
+ return kMasked;
+ }
+ std::string decoded;
+ if (!base64_decode(value.substr(pos + 1), &decoded)) {
+ return kMasked;
+ }
+ // Note that the password may contain a colon, so split on the first one
only.
+ auto colon = decoded.find(':');
+ if (colon == std::string::npos) {
+ return kMasked;
+ }
+ return decoded.substr(0, colon) + ":" + kMasked;
+}
+
HttpRequest::HttpRequest(evhttp_request* evhttp_request) :
_ev_req(evhttp_request) {}
HttpRequest::~HttpRequest() {
@@ -95,7 +124,8 @@ std::string HttpRequest::debug_string() const {
<< "headers: \n";
for (auto& iter : _headers) {
if (is_sensitive_header(iter.first)) {
- ss << "key=" << iter.first << ", value=***MASKED***\n";
+ ss << "key=" << iter.first
+ << ", value=" << mask_sensitive_header(iter.first, iter.second)
<< "\n";
} else {
ss << "key=" << iter.first << ", value=" << iter.second << "\n";
}
diff --git a/be/test/service/http/http_request_test.cpp
b/be/test/service/http/http_request_test.cpp
new file mode 100644
index 00000000000..fdb9df43f75
--- /dev/null
+++ b/be/test/service/http/http_request_test.cpp
@@ -0,0 +1,142 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "service/http/http_request.h"
+
+#include <event2/http.h>
+#include <gtest/gtest.h>
+
+#include <cstring>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "service/http/http_headers.h"
+#include "util/url_coding.h"
+
+namespace doris {
+
+namespace {
+
+constexpr char kMasked[] = "***MASKED***";
+
+std::string basic_of(const std::string& credentials) {
+ std::string encoded;
+ base64_encode(credentials, &encoded);
+ return "Basic " + encoded;
+}
+
+// Renders debug_string() for a request carrying a single header.
+std::string debug_string_with_header(const std::string& name, const
std::string& value) {
+ auto* evhttp_req = evhttp_request_new(nullptr, nullptr);
+ HttpRequest req(evhttp_req);
+ req.set_header(name, value);
+ std::string dumped = req.debug_string();
+ evhttp_request_free(evhttp_req);
+ return dumped;
+}
+
+} // namespace
+
+class HttpRequestTest : public testing::Test {};
+
+// The user name is what makes a request attributable, so it is kept while the
password is not.
+TEST_F(HttpRequestTest, basic_auth_keeps_user_name) {
+ const std::string header = basic_of("root:Secret123");
+ const std::string dumped =
debug_string_with_header(HttpHeaders::AUTHORIZATION, header);
+
+ EXPECT_NE(dumped.find("key=Authorization, value=root:***MASKED***"),
std::string::npos)
+ << dumped;
+ EXPECT_EQ(dumped.find("Secret123"), std::string::npos) << dumped;
+ // The base64 blob decodes to the password, so it must not survive either.
+ EXPECT_EQ(dumped.find(header.substr(strlen("Basic "))), std::string::npos)
<< dumped;
+}
+
+// The password may contain colons, so only the first one separates it from
the user name.
+TEST_F(HttpRequestTest, basic_auth_password_containing_colons) {
+ const std::string dumped =
+ debug_string_with_header(HttpHeaders::AUTHORIZATION,
basic_of("admin:pa:ss:word"));
+
+ EXPECT_NE(dumped.find("key=Authorization, value=admin:***MASKED***"),
std::string::npos)
+ << dumped;
+ EXPECT_EQ(dumped.find("pa:ss:word"), std::string::npos) << dumped;
+}
+
+// RFC 7617 makes the scheme token case insensitive.
+TEST_F(HttpRequestTest, basic_auth_scheme_is_case_insensitive) {
+ std::string encoded;
+ base64_encode(std::string("alice:secret"), &encoded);
+
+ for (const std::string& scheme : {"Basic", "basic", "BASIC", "BaSiC"}) {
+ const std::string dumped =
+ debug_string_with_header(HttpHeaders::AUTHORIZATION, scheme +
" " + encoded);
+ EXPECT_NE(dumped.find("value=alice:***MASKED***"), std::string::npos)
+ << "scheme=" << scheme << ", dumped=" << dumped;
+ EXPECT_EQ(dumped.find("secret"), std::string::npos) << "scheme=" <<
scheme;
+ }
+}
+
+// Anything that is not parseable as Basic credentials is masked as a whole,
so a malformed
+// header can never leak the part that would have been the password.
+TEST_F(HttpRequestTest, unparseable_credentials_are_masked_entirely) {
+ std::string encoded;
+ base64_encode(std::string("alice:secret"), &encoded);
+
+ const std::vector<std::pair<std::string, std::string>> cases = {
+ // more than one space between the scheme and the credentials
+ {"two spaces", "Basic " + encoded},
+ // no scheme at all
+ {"no scheme", encoded},
+ // a scheme that does not carry user:password
+ {"bearer", "Bearer eyJhbGciOiJIUzI1NiJ9.payload"},
+ // not decodable
+ {"bad base64", "Basic !!!not-base64!!!"},
+ // decodes, but carries no colon to split on
+ {"no colon", basic_of("no-colon-here")},
+ // empty value
+ {"empty", ""},
+ };
+
+ for (const auto& [name, value] : cases) {
+ const std::string dumped =
debug_string_with_header(HttpHeaders::AUTHORIZATION, value);
+ EXPECT_NE(dumped.find(std::string("key=Authorization, value=") +
kMasked),
+ std::string::npos)
+ << "case=" << name << ", dumped=" << dumped;
+ EXPECT_EQ(dumped.find("secret"), std::string::npos) << "case=" << name;
+ EXPECT_EQ(dumped.find("payload"), std::string::npos) << "case=" <<
name;
+ }
+}
+
+// Only Basic credentials carry a user name; every other sensitive header
stays fully masked.
+TEST_F(HttpRequestTest, other_sensitive_headers_are_masked_entirely) {
+ for (const std::string& name :
+ {std::string("token"), std::string("auth_code"),
std::string(HttpHeaders::AUTH_TOKEN),
+ std::string(HttpHeaders::PROXY_AUTHORIZATION)}) {
+ const std::string dumped = debug_string_with_header(name,
"SUPERSECRET123");
+ EXPECT_NE(dumped.find(kMasked), std::string::npos) << "header=" <<
name << ", " << dumped;
+ EXPECT_EQ(dumped.find("SUPERSECRET123"), std::string::npos) <<
"header=" << name;
+ }
+}
+
+TEST_F(HttpRequestTest, non_sensitive_headers_are_untouched) {
+ const std::string dumped =
debug_string_with_header(HttpHeaders::USER_AGENT, "curl/7.76.1");
+
+ EXPECT_NE(dumped.find("key=User-Agent, value=curl/7.76.1"),
std::string::npos) << dumped;
+ EXPECT_EQ(dumped.find(kMasked), std::string::npos) << dumped;
+}
+
+} // namespace doris
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/httpv2/controller/BaseController.java
b/fe/fe-core/src/main/java/org/apache/doris/httpv2/controller/BaseController.java
index 64cbfa54cb1..5a2b97e44d8 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/httpv2/controller/BaseController.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/httpv2/controller/BaseController.java
@@ -284,8 +284,11 @@ public class BaseController {
throws UnauthorizedException {
ActionAuthorizationInfo authInfo = new ActionAuthorizationInfo();
if (!parseAuthInfo(request, authInfo)) {
- LOG.info("parse auth info failed, Authorization header {}, url {}",
- request.getHeader("Authorization"),
request.getRequestURI());
+ // Never log the Authorization header itself: it carries
base64(user:password),
+ // which is trivially decodable. Only record whether it was absent
or malformed.
+ LOG.info("parse auth info failed, Authorization header is {}, url
{}",
+ Strings.isNullOrEmpty(request.getHeader("Authorization"))
? "absent" : "malformed",
+ request.getRequestURI());
throw new UnauthorizedException("Need auth information.");
}
if (LOG.isDebugEnabled()) {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]