fix(command): MONITOR command should redact sensitive tokens (#3193)
diff --git a/src/commands/cmd_function.cc b/src/commands/cmd_function.cc
index 73fc24c..3709a74 100644
--- a/src/commands/cmd_function.cc
+++ b/src/commands/cmd_function.cc
@@ -130,7 +130,7 @@
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 ea6a37d..e9a05fe 100644
--- a/src/commands/cmd_script.cc
+++ b/src/commands/cmd_script.cc
@@ -134,10 +134,12 @@
}
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 e90487d..212d44a 100644
--- a/src/commands/cmd_server.cc
+++ b/src/commands/cmd_server.cc
@@ -1555,8 +1555,8 @@
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 61a3b79..7fede57 100644
--- a/src/commands/commander.h
+++ b/src/commands/commander.h
@@ -87,6 +87,8 @@
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 @@
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 @@
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 06a1a45..e615486 100644
--- a/src/server/redis_connection.cc
+++ b/src/server/redis_connection.cc
@@ -573,7 +573,9 @@
}
}
- 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 6ca4014..04e62fe 100644
--- a/src/server/server.cc
+++ b/src/server/server.cc
@@ -370,13 +370,47 @@
}
}
+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 54f4c2c..7063a66 100644
--- a/src/server/server.h
+++ b/src/server/server.h
@@ -203,6 +203,7 @@
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 ff97d25..c8ddfc6 100644
--- a/src/storage/scripting.cc
+++ b/src/storage/scripting.cc
@@ -884,7 +884,9 @@
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 1b17a98..f8b0874 100644
--- a/tests/gocase/unit/introspection/introspection_test.go
+++ b/tests/gocase/unit/introspection/introspection_test.go
@@ -118,17 +118,6 @@
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 0000000..a76a84a
--- /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\).*`)
+}