This is an automated email from the ASF dual-hosted git repository.
hulk pushed a commit to branch unstable
in repository https://gitbox.apache.org/repos/asf/kvrocks.git
The following commit(s) were added to refs/heads/unstable by this push:
new d0a0d7124 fix(command): MONITOR command should redact sensitive tokens
(#3193)
d0a0d7124 is described below
commit d0a0d712434e7fc56b8a4f0acb8ea2e0159cd85a
Author: hulk <[email protected]>
AuthorDate: Sat Sep 20 21:39:15 2025 +0800
fix(command): MONITOR command should redact sensitive tokens (#3193)
---
src/commands/cmd_function.cc | 4 +-
src/commands/cmd_script.cc | 12 ++-
src/commands/cmd_server.cc | 4 +-
src/commands/commander.h | 5 +
src/server/redis_connection.cc | 4 +-
src/server/server.cc | 36 +++++++-
src/server/server.h | 1 +
src/storage/scripting.cc | 4 +-
.../unit/introspection/introspection_test.go | 11 ---
tests/gocase/unit/monitor/monitor_test.go | 102 +++++++++++++++++++++
10 files changed, 160 insertions(+), 23 deletions(-)
diff --git a/src/commands/cmd_function.cc b/src/commands/cmd_function.cc
index 73fc24c4b..3709a74ab 100644
--- a/src/commands/cmd_function.cc
+++ b/src/commands/cmd_function.cc
@@ -130,7 +130,7 @@ uint64_t GenerateFCallFlags(uint64_t flags, const
std::vector<std::string> &, co
REDIS_REGISTER_COMMANDS(
Function, MakeCmdAttr<CommandFunction>("function", -2, "exclusive
no-script", NO_KEY, GenerateFunctionFlags),
- MakeCmdAttr<CommandFCall<>>("fcall", -3, "write no-script",
GetScriptEvalKeyRange, GenerateFCallFlags),
- MakeCmdAttr<CommandFCall<true>>("fcall_ro", -3, "read-only no-script",
GetScriptEvalKeyRange));
+ MakeCmdAttr<CommandFCall<>>("fcall", -3, "write no-script skip-monitor",
GetScriptEvalKeyRange, GenerateFCallFlags),
+ MakeCmdAttr<CommandFCall<true>>("fcall_ro", -3, "read-only no-script
skip-monitor", GetScriptEvalKeyRange));
} // namespace redis
diff --git a/src/commands/cmd_script.cc b/src/commands/cmd_script.cc
index ea6a37d54..e9a05fecc 100644
--- a/src/commands/cmd_script.cc
+++ b/src/commands/cmd_script.cc
@@ -134,10 +134,12 @@ uint64_t GenerateEvalFlags(uint64_t flags, const
std::vector<std::string> &, con
}
REDIS_REGISTER_COMMANDS(
- Script, MakeCmdAttr<CommandEval>("eval", -3, "write no-script",
GetScriptEvalKeyRange, GenerateEvalFlags),
- MakeCmdAttr<CommandEvalSHA>("evalsha", -3, "write no-script",
GetScriptEvalKeyRange, GenerateEvalFlags),
- MakeCmdAttr<CommandEvalRO>("eval_ro", -3, "read-only no-script",
GetScriptEvalKeyRange),
- MakeCmdAttr<CommandEvalSHARO>("evalsha_ro", -3, "read-only no-script",
GetScriptEvalKeyRange),
- MakeCmdAttr<CommandScript>("script", -2, "exclusive no-script", NO_KEY,
GenerateScriptFlags), )
+ Script,
+ MakeCmdAttr<CommandEval>("eval", -3, "write no-script skip-monitor",
GetScriptEvalKeyRange, GenerateEvalFlags),
+ MakeCmdAttr<CommandEvalSHA>("evalsha", -3, "write no-script skip-monitor",
GetScriptEvalKeyRange,
+ GenerateEvalFlags),
+ MakeCmdAttr<CommandEvalRO>("eval_ro", -3, "read-only no-script
skip-monitor", GetScriptEvalKeyRange),
+ MakeCmdAttr<CommandEvalSHARO>("evalsha_ro", -3, "read-only no-script
skip-monitor", GetScriptEvalKeyRange),
+ MakeCmdAttr<CommandScript>("script", -2, "exclusive no-script
skip-monitor", NO_KEY, GenerateScriptFlags), )
} // namespace redis
diff --git a/src/commands/cmd_server.cc b/src/commands/cmd_server.cc
index e90487d67..212d44a49 100644
--- a/src/commands/cmd_server.cc
+++ b/src/commands/cmd_server.cc
@@ -1555,8 +1555,8 @@ REDIS_REGISTER_COMMANDS(
MakeCmdAttr<CommandSelect>("select", 2, "read-only", NO_KEY),
MakeCmdAttr<CommandInfo>("info", -1, "read-only ok-loading", NO_KEY),
MakeCmdAttr<CommandRole>("role", 1, "read-only ok-loading", NO_KEY),
- MakeCmdAttr<CommandConfig>("config", -2, "read-only admin", NO_KEY,
GenerateConfigFlag),
- MakeCmdAttr<CommandNamespace>("namespace", -2, "read-only admin", NO_KEY,
GenerateNamespaceFlag),
+ MakeCmdAttr<CommandConfig>("config", -2, "read-only admin skip-monitor",
NO_KEY, GenerateConfigFlag),
+ MakeCmdAttr<CommandNamespace>("namespace", -2, "read-only admin
skip-monitor", NO_KEY, GenerateNamespaceFlag),
MakeCmdAttr<CommandKeys>("keys", 2, "read-only slow", NO_KEY),
MakeCmdAttr<CommandFlushDB>("flushdb", 1, "write no-dbsize-check
exclusive", NO_KEY),
MakeCmdAttr<CommandFlushAll>("flushall", 1, "write no-dbsize-check
exclusive admin", NO_KEY),
diff --git a/src/commands/commander.h b/src/commands/commander.h
index 61a3b79d5..7fede573f 100644
--- a/src/commands/commander.h
+++ b/src/commands/commander.h
@@ -87,6 +87,8 @@ enum CommandFlags : uint64_t {
kCmdAuth = 1ULL << 10,
// "admin" flag, for commands that require admin permission
kCmdAdmin = 1ULL << 11,
+ // "skip-monitor" flag, for commands that should skip monitor feed
+ kCmdSkipMonitor = 1ULL << 12,
};
enum class CommandCategory : uint8_t {
@@ -352,6 +354,8 @@ inline uint64_t ParseCommandFlags(const std::string
&description, const std::str
flags |= kCmdNoMulti;
else if (flag == "no-script")
flags |= kCmdNoScript;
+ else if (flag == "skip-monitor")
+ flags |= kCmdSkipMonitor;
else if (flag == "no-dbsize-check")
flags |= kCmdNoDBSizeCheck;
else if (flag == "slow")
@@ -388,6 +392,7 @@ inline std::vector<std::string>
CommandAttributes::FlagsToString(uint64_t flags)
if (flags & kCmdBlocking) res.emplace_back("blocking");
if (flags & kCmdAuth) res.emplace_back("auth");
if (flags & kCmdAdmin) res.emplace_back("admin");
+ if (flags & kCmdSkipMonitor) res.emplace_back("skip-monitor");
return res;
}
diff --git a/src/server/redis_connection.cc b/src/server/redis_connection.cc
index 06a1a4559..e61548689 100644
--- a/src/server/redis_connection.cc
+++ b/src/server/redis_connection.cc
@@ -573,7 +573,9 @@ void Connection::ExecuteCommands(std::deque<CommandTokens>
*to_process_cmds) {
}
}
- srv_->FeedMonitorConns(this, cmd_tokens);
+ if (!(cmd_flags & redis::kCmdSkipMonitor)) {
+ srv_->FeedMonitorConns(this, cmd_tokens);
+ }
// Break the execution loop when occurring the blocking command like BLPOP
or BRPOP,
// it will suspend the connection and wait for the wakeup signal.
diff --git a/src/server/server.cc b/src/server/server.cc
index 6ca401438..04e62febc 100644
--- a/src/server/server.cc
+++ b/src/server/server.cc
@@ -370,13 +370,47 @@ void Server::CleanupExitedSlaves() {
}
}
+std::vector<std::string> Server::RedactSensitiveTokens(const
std::vector<std::string> &tokens) {
+ if (tokens.empty()) return tokens;
+ std::string cmd = util::ToLower(tokens[0]);
+ if (cmd != "auth" && cmd != "hello") return tokens;
+
+ std::vector<std::string> redacted_tokens = tokens;
+ if (cmd == "auth" && tokens.size() >= 2) {
+ // AUTH password -> redact password (arg 1)
+ redacted_tokens[1] = "(redacted)";
+ } else if (cmd == "hello" && tokens.size() >= 3) {
+ // HELLO [version] [AUTH [username] password] [SETNAME name]
+ for (size_t i = 1; i < tokens.size(); ++i) {
+ std::string arg = util::ToLower(tokens[i]);
+ if (arg == "auth" && i + 1 < tokens.size()) {
+ size_t remaining_args = tokens.size() - i - 1;
+ if (remaining_args >= 2) {
+ // Check if this follows the pattern AUTH username password
+ // In this case, redact the password (arg i+2)
+ redacted_tokens[i + 2] = "(redacted)";
+ } else if (remaining_args == 1) {
+ // This follows the pattern AUTH password
+ // Redact the password (arg i+1)
+ redacted_tokens[i + 1] = "(redacted)";
+ }
+ break;
+ }
+ }
+ }
+ return redacted_tokens;
+}
+
void Server::FeedMonitorConns(redis::Connection *conn, const
std::vector<std::string> &tokens) {
if (monitor_clients_ <= 0) return;
auto now_us = util::GetTimeStampUS();
std::string output =
fmt::format("{}.{} [{} {}]", now_us / 1000000, now_us % 1000000,
conn->GetNamespace(), conn->GetAddr());
- for (const auto &token : tokens) {
+
+ auto redacted_tokens = RedactSensitiveTokens(tokens);
+
+ for (const auto &token : redacted_tokens) {
output += " \"";
output += util::EscapeString(token);
output += "\"";
diff --git a/src/server/server.h b/src/server/server.h
index 54f4c2cc4..7063a6632 100644
--- a/src/server/server.h
+++ b/src/server/server.h
@@ -203,6 +203,7 @@ class Server {
void CleanupExitedSlaves();
bool IsSlave() const { return !master_host_.empty(); }
void FeedMonitorConns(redis::Connection *conn, const
std::vector<std::string> &tokens);
+ static std::vector<std::string> RedactSensitiveTokens(const
std::vector<std::string> &tokens);
void IncrFetchFileThread() { fetch_file_threads_num_++; }
void DecrFetchFileThread() { fetch_file_threads_num_--; }
int GetFetchFileThreadNum() const { return fetch_file_threads_num_; }
diff --git a/src/storage/scripting.cc b/src/storage/scripting.cc
index ff97d253c..c8ddfc667 100644
--- a/src/storage/scripting.cc
+++ b/src/storage/scripting.cc
@@ -884,7 +884,9 @@ int RedisGenericCommand(lua_State *lua, int raise_error) {
return raise_error ? RaiseError(lua) : 1;
}
- srv->FeedMonitorConns(conn, args);
+ if (!(cmd_flags & redis::kCmdSkipMonitor)) {
+ srv->FeedMonitorConns(conn, args);
+ }
RedisProtocolToLuaType(lua, output.data());
return 1;
diff --git a/tests/gocase/unit/introspection/introspection_test.go
b/tests/gocase/unit/introspection/introspection_test.go
index 1b17a98ca..f8b0874b5 100644
--- a/tests/gocase/unit/introspection/introspection_test.go
+++ b/tests/gocase/unit/introspection/introspection_test.go
@@ -118,17 +118,6 @@ func TestIntrospection(t *testing.T) {
require.Regexp(t, "id=.* addr=.*:.* fd=.* name=.* age=.*
idle=.* flags=N namespace=.* qbuf=.* .*obuf=.* cmd=client.*", v)
})
- t.Run("MONITOR can log executed commands", func(t *testing.T) {
- c := srv.NewTCPClient()
- defer func() { require.NoError(t, c.Close()) }()
- require.NoError(t, c.WriteArgs("MONITOR"))
- c.MustRead(t, "+OK")
- require.NoError(t, rdb.Set(ctx, "foo", "bar", 0).Err())
- require.NoError(t, rdb.Get(ctx, "foo").Err())
- c.MustMatch(t, ".*set.*foo.*bar.*")
- c.MustMatch(t, ".*get.*foo.*")
- })
-
t.Run("CLIENT GETNAME should return NIL if name is not assigned",
func(t *testing.T) {
require.EqualError(t, rdb.ClientGetName(ctx).Err(),
redis.Nil.Error())
})
diff --git a/tests/gocase/unit/monitor/monitor_test.go
b/tests/gocase/unit/monitor/monitor_test.go
new file mode 100644
index 000000000..a76a84a52
--- /dev/null
+++ b/tests/gocase/unit/monitor/monitor_test.go
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ */
+
+package monitor
+
+import (
+ "context"
+ "testing"
+
+ "github.com/redis/go-redis/v9"
+ "github.com/stretchr/testify/require"
+
+ "github.com/apache/kvrocks/tests/gocase/util"
+)
+
+func TestMonitor(t *testing.T) {
+ srv := util.StartServer(t, map[string]string{})
+ defer srv.Close()
+
+ ctx := context.Background()
+ rdb := srv.NewClient()
+ defer func() { require.NoError(t, rdb.Close()) }()
+
+ t.Run("MONITOR can log executed commands", func(t *testing.T) {
+ c := srv.NewTCPClient()
+ defer func() { require.NoError(t, c.Close()) }()
+ require.NoError(t, c.WriteArgs("MONITOR"))
+ c.MustRead(t, "+OK")
+ require.NoError(t, rdb.Set(ctx, "foo", "bar", 0).Err())
+ require.NoError(t, rdb.Get(ctx, "foo").Err())
+ c.MustMatch(t, ".*hello.*")
+ c.MustMatch(t, ".*set.*foo.*bar.*")
+ c.MustMatch(t, ".*get.*foo.*")
+ })
+
+ t.Run("MONITOR should skip commands with 'skip-monitor' flag", func(t
*testing.T) {
+ c := srv.NewTCPClient()
+ defer func() { require.NoError(t, c.Close()) }()
+ require.NoError(t, c.WriteArgs("MONITOR"))
+ c.MustRead(t, "+OK")
+
+ // Execute commands that should be skipped
+ require.NoError(t, rdb.ConfigSet(ctx, "slave-read-only",
"yes").Err())
+ require.NoError(t, rdb.ConfigGet(ctx, "slave-read-only").Err())
+
+ require.NoError(t, rdb.Ping(ctx).Err())
+ require.NoError(t, rdb.FlushAll(ctx).Err())
+ require.NoError(t, rdb.Set(ctx, "test", "value", 0).Err())
+ require.NoError(t, rdb.Get(ctx, "test").Err())
+ require.NoError(t, rdb.Info(ctx, "server").Err())
+ c.MustMatch(t, ".*ping.*")
+ c.MustMatch(t, ".*flushall.*")
+ c.MustMatch(t, ".*set.*test.*value.*")
+ c.MustMatch(t, ".*get.*test.*")
+ c.MustMatch(t, ".*info.*server.*")
+ })
+}
+
+func TestMonitorRedactPassword(t *testing.T) {
+ srv := util.StartServer(t, map[string]string{"requirepass": "testpass"})
+ defer srv.Close()
+
+ c := srv.NewTCPClient()
+ defer func() { require.NoError(t, c.Close()) }()
+ require.NoError(t, c.WriteArgs("AUTH", "testpass"))
+ c.MustRead(t, "+OK")
+
+ require.NoError(t, c.WriteArgs("MONITOR"))
+ c.MustRead(t, "+OK")
+
+ rdb := srv.NewClientWithOption(&redis.Options{
+ Password: "testpass",
+ })
+ defer func() { require.NoError(t, rdb.Close()) }()
+
+ ctx := context.Background()
+ require.NoError(t, rdb.Do(ctx, "AUTH", "testpass").Err())
+ require.NoError(t, rdb.Do(ctx, "HELLO", "3", "AUTH", "testpass").Err())
+ require.NoError(t, rdb.Do(ctx, "HELLO", "3", "AUTH", "default",
"testpass").Err())
+
+ // Client uses HELLO to AUTH, so it will have an extra HELLO command
+ c.MustMatch(t, `.*hello.*3.*auth.*default.*\(redacted\).*`)
+ c.MustMatch(t, `.*AUTH.*\(redacted\).*`)
+ c.MustMatch(t, `.*HELLO.*3.*AUTH.*\(redacted\).*`)
+ c.MustMatch(t, `.*HELLO.*3.*AUTH.*default.*\(redacted\).*`)
+}