From 47dc7fe816006dfc49bdcfca74e66c5ba46d4de8 Mon Sep 17 00:00:00 2001 From: Devin Robison Date: Tue, 24 Mar 2026 14:58:39 -0700 Subject: [PATCH] fix: blcok non-owner authorized senders from chaning /send policy (#53994) --- src/auto-reply/reply/commands-session.ts | 13 ++-- src/auto-reply/reply/commands.test.ts | 95 ++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 5 deletions(-) diff --git a/src/auto-reply/reply/commands-session.ts b/src/auto-reply/reply/commands-session.ts index d897a56db02..4199f14c6bc 100644 --- a/src/auto-reply/reply/commands-session.ts +++ b/src/auto-reply/reply/commands-session.ts @@ -18,6 +18,7 @@ import { isTelegramSurface, resolveChannelAccountId, } from "./channel-context.js"; +import { rejectNonOwnerCommand, rejectUnauthorizedCommand } from "./command-gates.js"; import { handleAbortTrigger, handleStopCommand } from "./commands-session-abort.js"; import { persistSessionEntry } from "./commands-session-store.js"; import type { CommandHandler } from "./commands-types.js"; @@ -217,11 +218,13 @@ export const handleSendPolicyCommand: CommandHandler = async (params, allowTextC if (!sendPolicyCommand.hasCommand) { return null; } - if (!params.command.isAuthorizedSender) { - logVerbose( - `Ignoring /send from unauthorized sender: ${params.command.senderId || ""}`, - ); - return { shouldContinue: false }; + const unauthorizedResult = rejectUnauthorizedCommand(params, "/send"); + if (unauthorizedResult) { + return unauthorizedResult; + } + const nonOwnerResult = rejectNonOwnerCommand(params, "/send"); + if (nonOwnerResult) { + return nonOwnerResult; } if (!sendPolicyCommand.mode) { return { diff --git a/src/auto-reply/reply/commands.test.ts b/src/auto-reply/reply/commands.test.ts index 9336da1590a..2e7461da57b 100644 --- a/src/auto-reply/reply/commands.test.ts +++ b/src/auto-reply/reply/commands.test.ts @@ -867,6 +867,101 @@ describe("handleCommands owner gating for privileged show commands", () => { }); }); +describe("handleCommands /send owner gating", () => { + it("blocks authorized non-owner senders from mutating session send policy", async () => { + const params = buildParams("/send off", { + commands: { text: true }, + channels: { whatsapp: { allowFrom: ["*"] } }, + } as OpenClawConfig); + params.command.senderIsOwner = false; + + const sessionEntry: SessionEntry = { + sessionId: "session-send-policy", + updatedAt: Date.now(), + sendPolicy: "allow", + }; + const sessionStore: Record = { + [params.sessionKey]: sessionEntry, + }; + + const result = await handleCommands({ + ...params, + sessionEntry, + sessionStore, + }); + + expect(result).toEqual({ shouldContinue: false }); + expect(sessionEntry.sendPolicy).toBe("allow"); + expect(sessionStore[params.sessionKey]?.sendPolicy).toBe("allow"); + }); + + it("allows owners to mutate session send policy", async () => { + const params = buildParams("/send off", { + commands: { text: true }, + channels: { whatsapp: { allowFrom: ["*"] } }, + } as OpenClawConfig); + params.command.senderIsOwner = true; + + const sessionEntry: SessionEntry = { + sessionId: "session-send-policy-owner", + updatedAt: Date.now(), + sendPolicy: "allow", + }; + const sessionStore: Record = { + [params.sessionKey]: sessionEntry, + }; + + const result = await handleCommands({ + ...params, + sessionEntry, + sessionStore, + }); + + expect(result.shouldContinue).toBe(false); + expect(result.reply?.text).toContain("Send policy set to off"); + expect(sessionEntry.sendPolicy).toBe("deny"); + expect(sessionStore[params.sessionKey]?.sendPolicy).toBe("deny"); + }); + + it("returns an explicit unauthorized reply for native /send from non-owners", async () => { + const params = buildParams( + "/send off", + { + commands: { text: true }, + channels: { discord: { dm: { enabled: true, policy: "open" } } }, + } as OpenClawConfig, + { + Provider: "discord", + Surface: "discord", + CommandSource: "native", + }, + ); + params.command.senderIsOwner = false; + + const sessionEntry: SessionEntry = { + sessionId: "session-send-policy-native", + updatedAt: Date.now(), + sendPolicy: "allow", + }; + const sessionStore: Record = { + [params.sessionKey]: sessionEntry, + }; + + const result = await handleCommands({ + ...params, + sessionEntry, + sessionStore, + }); + + expect(result).toEqual({ + shouldContinue: false, + reply: { text: "You are not authorized to use this command." }, + }); + expect(sessionEntry.sendPolicy).toBe("allow"); + expect(sessionStore[params.sessionKey]?.sendPolicy).toBe("allow"); + }); +}); + describe("handleCommands /config configWrites gating", () => { it("blocks disallowed /config set writes", async () => { const cases = [