fix(redis): block FLUSHDB in MCP instead of running it unconfirmed

This commit is contained in:
BASIL K AJI
2026-10-04 03:43:49 +08:00
committed by GitHub
parent a9990d5fdb
commit 92a29cb715
7 changed files with 11 additions and 12 deletions
@@ -25,7 +25,7 @@ describe("AI Redis console routing", () => {
it("uses the console safety path and rejects unavailable command input", () => {
expect(classifyRedisCommandSafety("CONFIG SET requirepass secret")).toBe("blocked");
expect(classifyRedisCommandSafety("FLUSHDB")).toBe("confirm");
expect(classifyRedisCommandSafety("FLUSHDB")).toBe("blocked");
expect(classifyRedisCommandSafety("SET issue:846 fixed")).toBe("write");
expect(classifyRedisCommandSafety("INFO server")).toBe("allowed");
@@ -2,7 +2,7 @@ import { isRedisMonitorCommand } from "./redisMonitor";
export type RedisCommandSafety = "allowed" | "write" | "confirm" | "blocked";
const BLOCKED_COMMANDS = new Set(["KEYS", "FLUSHALL", "SHUTDOWN", "CONFIG", "SAVE", "BGSAVE", "SLAVEOF", "REPLICAOF", "MIGRATE", "MODULE", "SCRIPT", "EVAL", "EVALSHA"]);
const BLOCKED_COMMANDS = new Set(["KEYS", "FLUSHALL", "FLUSHDB", "SHUTDOWN", "CONFIG", "SAVE", "BGSAVE", "SLAVEOF", "REPLICAOF", "MIGRATE", "MODULE", "SCRIPT", "EVAL", "EVALSHA"]);
// Explicit allowlist matching the backend's read-command classification.
// Unknown commands fall through to "blocked" (fail-closed).
@@ -199,7 +199,6 @@ const CONFIRM_COMMANDS = new Set([
"ZUNIONSTORE",
"PFMERGE",
"GEOSEARCHSTORE",
"FLUSHDB",
]);
const WRITE_COMMANDS = new Set([
@@ -377,7 +377,7 @@ const RAW_COMMANDS: Record<string, Spec> = {
DBSIZE: [1, "server"],
FAILOVER: [-1, "server", "confirm"],
FLUSHALL: [-1, "server", "blocked"],
FLUSHDB: [-1, "server", "confirm"],
FLUSHDB: [-1, "server", "blocked"],
INFO: [-1, "server"],
LASTSAVE: [1, "server"],
"MEMORY USAGE": [-3, "server"],
+2 -2
View File
@@ -2134,7 +2134,7 @@ pub fn classify_command(command: &str) -> RedisCommandSafety {
| "ZREM" | "ZPOPMAX" | "ZPOPMIN" | "ZMPOP" | "BZMPOP" | "BZPOPMAX" | "BZPOPMIN" | "ZREMRANGEBYLEX"
| "ZREMRANGEBYRANK" | "ZREMRANGEBYSCORE" | "XDEL" | "XTRIM" | "MOVE" | "SORT" | "SDIFFSTORE"
| "SINTERSTORE" | "SUNIONSTORE" | "ZDIFFSTORE" | "ZINTERSTORE" | "ZRANGESTORE" | "ZUNIONSTORE" | "PFMERGE"
| "GEOSEARCHSTORE" | "FLUSHDB" => RedisCommandSafety::Confirm,
| "GEOSEARCHSTORE" => RedisCommandSafety::Confirm,
"APPEND" | "BITFIELD" | "BITOP" | "COPY" | "DECR" | "DECRBY" | "GEOADD" | "GEORADIUS" | "GEORADIUSBYMEMBER"
| "GETEX" | "GETSET" | "INCR" | "INCRBY" | "INCRBYFLOAT" | "SET" | "SETEX" | "PSETEX" | "SETNX"
| "SETRANGE" | "MSET" | "MSETNX" | "PERSIST" | "HSET" | "HMSET" | "HINCRBY" | "HINCRBYFLOAT" | "HSETNX"
@@ -7686,7 +7686,7 @@ mod tests {
assert_eq!(classify_command("GETEX"), RedisCommandSafety::Write);
assert_eq!(classify_command("XREADGROUP"), RedisCommandSafety::Write);
assert_eq!(classify_command("del"), RedisCommandSafety::Confirm);
assert_eq!(classify_command("flushdb"), RedisCommandSafety::Confirm);
assert_eq!(classify_command("flushdb"), RedisCommandSafety::Blocked);
assert_eq!(classify_command("JSON.DEL"), RedisCommandSafety::Confirm);
assert_eq!(classify_command("JSON.FORGET"), RedisCommandSafety::Confirm);
assert_eq!(classify_command("JSON.CLEAR"), RedisCommandSafety::Confirm);
+1 -1
View File
@@ -7,7 +7,7 @@ test("classifies normal Redis writes separately from destructive commands", () =
assert.equal(classifyRedisCommandSafety("HSET hash field value"), "write");
assert.equal(classifyRedisCommandSafety("LPUSH queue value"), "write");
assert.equal(classifyRedisCommandSafety("DEL session:1"), "confirm");
assert.equal(classifyRedisCommandSafety("FLUSHDB"), "confirm");
assert.equal(classifyRedisCommandSafety("FLUSHDB"), "blocked");
assert.equal(classifyRedisCommandSafety("KEYS *"), "blocked");
assert.equal(classifyRedisCommandSafety("GET session:1"), "allowed");
});
+1 -1
View File
@@ -83,5 +83,5 @@ test("normal writes do not require confirmation but destructive commands do", ()
assert.equal(resolveRedisCommandSpec(["HSET"])?.safety, "write");
assert.equal(resolveRedisCommandSpec(["LPUSH"])?.safety, "write");
assert.equal(resolveRedisCommandSpec(["DEL"])?.safety, "confirm");
assert.equal(resolveRedisCommandSpec(["FLUSHDB"])?.safety, "confirm");
assert.equal(resolveRedisCommandSpec(["FLUSHDB"])?.safety, "blocked");
});
+4 -4
View File
@@ -71,8 +71,8 @@ test("highlights destructive commands as warning", () => {
assert.match(diags[0].message, /Dangerous command 'DEL'/);
});
test("highlights flushall as blocked error but flushdb as confirm warning", () => {
// Aligned with the execution safety classification: FLUSHALL=blocked, FLUSHDB=confirm.
test("highlights flushall and flushdb as blocked errors", () => {
// Aligned with the execution safety classification: FLUSHALL/FLUSHDB=blocked.
const all = buildRedisSyntaxDiagnostics("FLUSHALL");
assert.equal(all.length, 1);
assert.equal(all[0].severity, "error");
@@ -80,8 +80,8 @@ test("highlights flushall as blocked error but flushdb as confirm warning", () =
const db = buildRedisSyntaxDiagnostics("FLUSHDB");
assert.equal(db.length, 1);
assert.equal(db[0].severity, "warning");
assert.match(db[0].message, /Dangerous command 'FLUSHDB'/);
assert.equal(db[0].severity, "error");
assert.match(db[0].message, /Blocked command 'FLUSHDB'/);
});
test("subcommands resolve via MAIN SUB key", () => {