fix(security): default-deny command execution

This commit is contained in:
Peter Steinberger
2026-01-17 08:27:52 +00:00
parent d8b463d0b3
commit 56f3a2de25
36 changed files with 247 additions and 46 deletions

View File

@@ -1,7 +1,12 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import type { ResolvedZaloAccount } from "./accounts.js";
import {
hasInlineCommandTokens,
isControlCommandMessage,
} from "../../../src/auto-reply/command-detection.js";
import { finalizeInboundContext } from "../../../src/auto-reply/reply/inbound-context.js";
import { resolveCommandAuthorizedFromAuthorizers } from "../../../src/channels/command-gating.js";
import {
ZaloApiError,
deleteWebhook,
@@ -437,6 +442,22 @@ async function processMessageWithPipeline(params: {
const dmPolicy = account.config.dmPolicy ?? "pairing";
const configAllowFrom = (account.config.allowFrom ?? []).map((v) => String(v));
const rawBody = text?.trim() || (mediaPath ? "<media:image>" : "");
const shouldComputeCommandAuthorized =
isControlCommandMessage(rawBody, config) || hasInlineCommandTokens(rawBody);
const storeAllowFrom =
!isGroup && (dmPolicy !== "open" || shouldComputeCommandAuthorized)
? await deps.readChannelAllowFromStore("zalo").catch(() => [])
: [];
const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom];
const useAccessGroups = config.commands?.useAccessGroups !== false;
const senderAllowedForCommands = isSenderAllowed(senderId, effectiveAllowFrom);
const commandAuthorized = shouldComputeCommandAuthorized
? resolveCommandAuthorizedFromAuthorizers({
useAccessGroups,
authorizers: [{ configured: effectiveAllowFrom.length > 0, allowed: senderAllowedForCommands }],
})
: undefined;
if (!isGroup) {
if (dmPolicy === "disabled") {
@@ -445,9 +466,7 @@ async function processMessageWithPipeline(params: {
}
if (dmPolicy !== "open") {
const storeAllowFrom = await deps.readChannelAllowFromStore("zalo").catch(() => []);
const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom];
const allowed = isSenderAllowed(senderId, effectiveAllowFrom);
const allowed = senderAllowedForCommands;
if (!allowed) {
if (dmPolicy === "pairing") {
@@ -496,7 +515,11 @@ async function processMessageWithPipeline(params: {
},
});
const rawBody = text?.trim() || (mediaPath ? "<media:image>" : "");
if (isGroup && isControlCommandMessage(rawBody, config) && commandAuthorized !== true) {
logVerbose(deps, `zalo: drop control command from unauthorized sender ${senderId}`);
return;
}
const fromLabel = isGroup
? `group:${chatId}`
: senderName || `user:${senderId}`;
@@ -519,6 +542,7 @@ async function processMessageWithPipeline(params: {
ConversationLabel: fromLabel,
SenderName: senderName || undefined,
SenderId: senderId,
CommandAuthorized: commandAuthorized,
Provider: "zalo",
Surface: "zalo",
MessageSid: message_id,

View File

@@ -1,7 +1,12 @@
import type { ChildProcess } from "node:child_process";
import type { RuntimeEnv } from "../../../src/runtime.js";
import {
hasInlineCommandTokens,
isControlCommandMessage,
} from "../../../src/auto-reply/command-detection.js";
import { finalizeInboundContext } from "../../../src/auto-reply/reply/inbound-context.js";
import { resolveCommandAuthorizedFromAuthorizers } from "../../../src/channels/command-gating.js";
import { loadCoreChannelDeps, type CoreChannelDeps } from "./core-bridge.js";
import { sendMessageZalouser } from "./send.js";
import type { CoreConfig, ResolvedZalouserAccount, ZcaMessage } from "./types.js";
@@ -105,6 +110,22 @@ async function processMessage(
const dmPolicy = account.config.dmPolicy ?? "pairing";
const configAllowFrom = (account.config.allowFrom ?? []).map((v) => String(v));
const rawBody = content.trim();
const shouldComputeCommandAuthorized =
isControlCommandMessage(rawBody, config) || hasInlineCommandTokens(rawBody);
const storeAllowFrom =
!isGroup && (dmPolicy !== "open" || shouldComputeCommandAuthorized)
? await deps.readChannelAllowFromStore("zalouser").catch(() => [])
: [];
const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom];
const useAccessGroups = config.commands?.useAccessGroups !== false;
const senderAllowedForCommands = isSenderAllowed(senderId, effectiveAllowFrom);
const commandAuthorized = shouldComputeCommandAuthorized
? resolveCommandAuthorizedFromAuthorizers({
useAccessGroups,
authorizers: [{ configured: effectiveAllowFrom.length > 0, allowed: senderAllowedForCommands }],
})
: undefined;
if (!isGroup) {
if (dmPolicy === "disabled") {
@@ -113,9 +134,7 @@ async function processMessage(
}
if (dmPolicy !== "open") {
const storeAllowFrom = await deps.readChannelAllowFromStore("zalouser").catch(() => []);
const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom];
const allowed = isSenderAllowed(senderId, effectiveAllowFrom);
const allowed = senderAllowedForCommands;
if (!allowed) {
if (dmPolicy === "pairing") {
@@ -158,6 +177,11 @@ async function processMessage(
}
}
if (isGroup && isControlCommandMessage(rawBody, config) && commandAuthorized !== true) {
logVerbose(deps, runtime, `zalouser: drop control command from unauthorized sender ${senderId}`);
return;
}
const peer = isGroup ? { kind: "group" as const, id: chatId } : { kind: "group" as const, id: senderId };
const route = deps.resolveAgentRoute({
@@ -171,7 +195,6 @@ async function processMessage(
},
});
const rawBody = content.trim();
const fromLabel = isGroup
? `group:${chatId}`
: senderName || `user:${senderId}`;
@@ -194,6 +217,7 @@ async function processMessage(
ConversationLabel: fromLabel,
SenderName: senderName || undefined,
SenderId: senderId,
CommandAuthorized: commandAuthorized,
Provider: "zalouser",
Surface: "zalouser",
MessageSid: message.msgId ?? `${timestamp}`,

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { hasControlCommand } from "./command-detection.js";
import { hasControlCommand, hasInlineCommandTokens } from "./command-detection.js";
import { listChatCommands } from "./commands-registry.js";
import { parseActivationCommand } from "./group-activation.js";
import { parseSendPolicyCommand } from "./send-policy.js";
@@ -72,6 +72,14 @@ describe("control command parsing", () => {
expect(hasControlCommand("/send on")).toBe(true);
});
it("detects inline command tokens", () => {
expect(hasInlineCommandTokens("hello /status")).toBe(true);
expect(hasInlineCommandTokens("hey /think high")).toBe(true);
expect(hasInlineCommandTokens("plain text")).toBe(false);
expect(hasInlineCommandTokens("http://example.com/path")).toBe(false);
expect(hasInlineCommandTokens("stop")).toBe(false);
});
it("ignores telegram commands addressed to other bots", () => {
expect(
hasControlCommand("/help@otherbot", undefined, {

View File

@@ -45,3 +45,16 @@ export function isControlCommandMessage(
const normalized = normalizeCommandBody(trimmed, options).trim().toLowerCase();
return isAbortTrigger(normalized);
}
/**
* Coarse detection for inline directives/shortcuts (e.g. "hey /status") so channel monitors
* can decide whether to compute CommandAuthorized for a message.
*
* This intentionally errs on the side of false positives; CommandAuthorized only gates
* command/directive execution, not normal chat replies.
*/
export function hasInlineCommandTokens(text?: string): boolean {
const body = text ?? "";
if (!body.trim()) return false;
return /(?:^|\s)[/!][a-z]/i.test(body);
}

View File

@@ -81,6 +81,7 @@ describe("directive behavior", () => {
Body: "/thinking xhigh",
From: "+1004",
To: "+2000",
CommandAuthorized: true,
},
{},
{
@@ -90,7 +91,7 @@ describe("directive behavior", () => {
workspace: path.join(home, "clawd"),
},
},
whatsapp: { allowFrom: ["*"] },
channels: { whatsapp: { allowFrom: ["*"] } },
session: { store: storePath },
},
);
@@ -108,6 +109,7 @@ describe("directive behavior", () => {
Body: "/thinking xhigh",
From: "+1004",
To: "+2000",
CommandAuthorized: true,
},
{},
{
@@ -117,7 +119,7 @@ describe("directive behavior", () => {
workspace: path.join(home, "clawd"),
},
},
whatsapp: { allowFrom: ["*"] },
channels: { whatsapp: { allowFrom: ["*"] } },
session: { store: storePath },
},
);
@@ -135,6 +137,7 @@ describe("directive behavior", () => {
Body: "/thinking xhigh",
From: "+1004",
To: "+2000",
CommandAuthorized: true,
},
{},
{
@@ -144,7 +147,7 @@ describe("directive behavior", () => {
workspace: path.join(home, "clawd"),
},
},
whatsapp: { allowFrom: ["*"] },
channels: { whatsapp: { allowFrom: ["*"] } },
session: { store: storePath },
},
);
@@ -164,6 +167,7 @@ describe("directive behavior", () => {
Body: "/help",
From: "+1222",
To: "+1222",
CommandAuthorized: true,
},
{},
{
@@ -201,6 +205,7 @@ describe("directive behavior", () => {
Body: "/demo_skill",
From: "+1222",
To: "+1222",
CommandAuthorized: true,
},
{},
{
@@ -232,6 +237,7 @@ describe("directive behavior", () => {
Body: "/queue collect debounce:bogus cap:zero drop:maybe",
From: "+1222",
To: "+1222",
CommandAuthorized: true,
},
{},
{
@@ -263,6 +269,7 @@ describe("directive behavior", () => {
From: "+1222",
To: "+1222",
Provider: "whatsapp",
CommandAuthorized: true,
},
{},
{
@@ -300,7 +307,7 @@ describe("directive behavior", () => {
vi.mocked(runEmbeddedPiAgent).mockReset();
const res = await getReplyFromConfig(
{ Body: "/think", From: "+1222", To: "+1222" },
{ Body: "/think", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {

View File

@@ -173,7 +173,7 @@ describe("directive behavior", () => {
vi.mocked(runEmbeddedPiAgent).mockReset();
const res = await getReplyFromConfig(
{ Body: "/verbose on", From: "+1222", To: "+1222" },
{ Body: "/verbose on", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {
@@ -197,7 +197,7 @@ describe("directive behavior", () => {
const storePath = path.join(home, "sessions.json");
const res = await getReplyFromConfig(
{ Body: "/verbose off", From: "+1222", To: "+1222" },
{ Body: "/verbose off", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {
@@ -223,7 +223,7 @@ describe("directive behavior", () => {
vi.mocked(runEmbeddedPiAgent).mockReset();
const res = await getReplyFromConfig(
{ Body: "/think", From: "+1222", To: "+1222" },
{ Body: "/think", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {
@@ -248,7 +248,7 @@ describe("directive behavior", () => {
vi.mocked(runEmbeddedPiAgent).mockReset();
const res = await getReplyFromConfig(
{ Body: "/think", From: "+1222", To: "+1222" },
{ Body: "/think", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {

View File

@@ -73,7 +73,7 @@ describe("directive behavior", () => {
]);
const res = await getReplyFromConfig(
{ Body: "/think", From: "+1222", To: "+1222" },
{ Body: "/think", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {
@@ -105,7 +105,7 @@ describe("directive behavior", () => {
]);
const res = await getReplyFromConfig(
{ Body: "/think", From: "+1222", To: "+1222" },
{ Body: "/think", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {

View File

@@ -66,7 +66,7 @@ describe("directive behavior", () => {
const storePath = path.join(home, "sessions.json");
const res = await getReplyFromConfig(
{ Body: "/model list", From: "+1222", To: "+1222" },
{ Body: "/model list", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {
@@ -97,7 +97,7 @@ describe("directive behavior", () => {
const storePath = path.join(home, "sessions.json");
const res = await getReplyFromConfig(
{ Body: "/model", From: "+1222", To: "+1222" },
{ Body: "/model", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {
@@ -137,7 +137,7 @@ describe("directive behavior", () => {
const storePath = path.join(home, "sessions.json");
const res = await getReplyFromConfig(
{ Body: "/model list", From: "+1222", To: "+1222" },
{ Body: "/model list", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {
@@ -178,7 +178,7 @@ describe("directive behavior", () => {
const storePath = path.join(home, "sessions.json");
const res = await getReplyFromConfig(
{ Body: "/model list", From: "+1222", To: "+1222" },
{ Body: "/model list", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {
@@ -205,7 +205,7 @@ describe("directive behavior", () => {
const storePath = path.join(home, "sessions.json");
await getReplyFromConfig(
{ Body: "/model openai/gpt-4.1-mini", From: "+1222", To: "+1222" },
{ Body: "/model openai/gpt-4.1-mini", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {
@@ -235,7 +235,7 @@ describe("directive behavior", () => {
const storePath = path.join(home, "sessions.json");
await getReplyFromConfig(
{ Body: "/model Opus", From: "+1222", To: "+1222" },
{ Body: "/model Opus", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {

View File

@@ -68,7 +68,7 @@ describe("directive behavior", () => {
const storePath = path.join(home, "sessions.json");
await getReplyFromConfig(
{ Body: "/model ki", From: "+1222", To: "+1222" },
{ Body: "/model ki", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {
@@ -135,7 +135,7 @@ describe("directive behavior", () => {
);
const res = await getReplyFromConfig(
{ Body: "/model Opus@anthropic:work", From: "+1222", To: "+1222" },
{ Body: "/model Opus@anthropic:work", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {
@@ -167,7 +167,7 @@ describe("directive behavior", () => {
const storePath = path.join(home, "sessions.json");
await getReplyFromConfig(
{ Body: "/model Opus", From: "+1222", To: "+1222" },
{ Body: "/model Opus", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {
@@ -200,6 +200,7 @@ describe("directive behavior", () => {
From: "+1222",
To: "+1222",
Provider: "whatsapp",
CommandAuthorized: true,
},
{},
{
@@ -230,6 +231,7 @@ describe("directive behavior", () => {
From: "+1222",
To: "+1222",
Provider: "whatsapp",
CommandAuthorized: true,
},
{},
{

View File

@@ -72,6 +72,7 @@ describe("directive behavior", () => {
Provider: "whatsapp",
SenderE164: "+1222",
SessionKey: "agent:work:main",
CommandAuthorized: true,
},
{},
{
@@ -118,6 +119,7 @@ describe("directive behavior", () => {
Provider: "whatsapp",
SenderE164: "+1333",
SessionKey: "agent:work:main",
CommandAuthorized: true,
},
{},
{
@@ -163,6 +165,7 @@ describe("directive behavior", () => {
To: "+1222",
Provider: "whatsapp",
SenderE164: "+1222",
CommandAuthorized: true,
},
{},
{
@@ -200,6 +203,7 @@ describe("directive behavior", () => {
To: "+1222",
Provider: "whatsapp",
SenderE164: "+1222",
CommandAuthorized: true,
},
{},
{
@@ -235,6 +239,7 @@ describe("directive behavior", () => {
To: "+1222",
Provider: "whatsapp",
SenderE164: "+1222",
CommandAuthorized: true,
},
{},
{

View File

@@ -72,6 +72,7 @@ describe("directive behavior", () => {
To: "+1222",
Provider: "whatsapp",
SenderE164: "+1222",
CommandAuthorized: true,
},
{},
{
@@ -115,6 +116,7 @@ describe("directive behavior", () => {
Provider: "whatsapp",
SenderE164: "+1222",
SessionKey: "agent:restricted:main",
CommandAuthorized: true,
},
{},
{
@@ -153,7 +155,7 @@ describe("directive behavior", () => {
const storePath = path.join(home, "sessions.json");
const res = await getReplyFromConfig(
{ Body: "/queue interrupt", From: "+1222", To: "+1222" },
{ Body: "/queue interrupt", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {
@@ -185,6 +187,7 @@ describe("directive behavior", () => {
Body: "/queue collect debounce:2s cap:5 drop:old",
From: "+1222",
To: "+1222",
CommandAuthorized: true,
},
{},
{
@@ -219,7 +222,7 @@ describe("directive behavior", () => {
const storePath = path.join(home, "sessions.json");
await getReplyFromConfig(
{ Body: "/queue interrupt", From: "+1222", To: "+1222" },
{ Body: "/queue interrupt", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {
@@ -234,7 +237,7 @@ describe("directive behavior", () => {
);
const res = await getReplyFromConfig(
{ Body: "/queue reset", From: "+1222", To: "+1222" },
{ Body: "/queue reset", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {

View File

@@ -72,6 +72,7 @@ describe("directive behavior", () => {
To: "+1222",
Provider: "whatsapp",
SenderE164: "+1222",
CommandAuthorized: true,
},
{},
{
@@ -99,6 +100,7 @@ describe("directive behavior", () => {
To: "+1222",
Provider: "whatsapp",
SenderE164: "+1222",
CommandAuthorized: true,
},
{},
{
@@ -153,6 +155,7 @@ describe("directive behavior", () => {
To: "+1222",
Provider: "whatsapp",
SenderE164: "+1222",
CommandAuthorized: true,
},
{},
cfg,
@@ -164,6 +167,7 @@ describe("directive behavior", () => {
To: "+1222",
Provider: "whatsapp",
SenderE164: "+1222",
CommandAuthorized: true,
},
{},
cfg,
@@ -176,6 +180,7 @@ describe("directive behavior", () => {
To: "+1222",
Provider: "whatsapp",
SenderE164: "+1222",
CommandAuthorized: true,
},
{},
cfg,
@@ -203,6 +208,7 @@ describe("directive behavior", () => {
Provider: "whatsapp",
SenderE164: "+1222",
SessionKey: "agent:restricted:main",
CommandAuthorized: true,
},
{},
{

View File

@@ -65,7 +65,7 @@ describe("directive behavior", () => {
vi.mocked(runEmbeddedPiAgent).mockReset();
const res = await getReplyFromConfig(
{ Body: "/verbose", From: "+1222", To: "+1222" },
{ Body: "/verbose", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {
@@ -90,7 +90,7 @@ describe("directive behavior", () => {
vi.mocked(runEmbeddedPiAgent).mockReset();
const res = await getReplyFromConfig(
{ Body: "/reasoning", From: "+1222", To: "+1222" },
{ Body: "/reasoning", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {
@@ -120,6 +120,7 @@ describe("directive behavior", () => {
To: "+1222",
Provider: "whatsapp",
SenderE164: "+1222",
CommandAuthorized: true,
},
{},
{
@@ -158,6 +159,7 @@ describe("directive behavior", () => {
To: "+1222",
Provider: "whatsapp",
SenderE164: "+1222",
CommandAuthorized: true,
},
{},
{

View File

@@ -66,7 +66,7 @@ describe("directive behavior", () => {
const storePath = path.join(home, "sessions.json");
await getReplyFromConfig(
{ Body: "/model kimi", From: "+1222", To: "+1222" },
{ Body: "/model kimi", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {
@@ -107,7 +107,7 @@ describe("directive behavior", () => {
const storePath = path.join(home, "sessions.json");
await getReplyFromConfig(
{ Body: "/model kimi-k2-0905-preview", From: "+1222", To: "+1222" },
{ Body: "/model kimi-k2-0905-preview", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {
@@ -148,7 +148,7 @@ describe("directive behavior", () => {
const storePath = path.join(home, "sessions.json");
await getReplyFromConfig(
{ Body: "/model moonshot/kimi", From: "+1222", To: "+1222" },
{ Body: "/model moonshot/kimi", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {
@@ -189,7 +189,7 @@ describe("directive behavior", () => {
const storePath = path.join(home, "sessions.json");
await getReplyFromConfig(
{ Body: "/model minimax", From: "+1222", To: "+1222" },
{ Body: "/model minimax", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {
@@ -234,7 +234,7 @@ describe("directive behavior", () => {
const storePath = path.join(home, "sessions.json");
await getReplyFromConfig(
{ Body: "/model minimax/m2.1", From: "+1222", To: "+1222" },
{ Body: "/model minimax/m2.1", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {

View File

@@ -153,7 +153,7 @@ describe("directive behavior", () => {
});
await getReplyFromConfig(
{ Body: "/verbose on", From: ctx.From, To: ctx.To },
{ Body: "/verbose on", From: ctx.From, To: ctx.To, CommandAuthorized: true },
{},
{
agents: {
@@ -193,7 +193,7 @@ describe("directive behavior", () => {
const storePath = path.join(home, "sessions.json");
const res = await getReplyFromConfig(
{ Body: "/model", From: "+1222", To: "+1222" },
{ Body: "/model", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {
@@ -224,7 +224,7 @@ describe("directive behavior", () => {
const storePath = path.join(home, "sessions.json");
const res = await getReplyFromConfig(
{ Body: "/model status", From: "+1222", To: "+1222" },
{ Body: "/model status", From: "+1222", To: "+1222", CommandAuthorized: true },
{},
{
agents: {

View File

@@ -54,6 +54,7 @@ describe("RawBody directive parsing", () => {
From: "+1222",
To: "+1222",
ChatType: "group",
CommandAuthorized: true,
};
const res = await getReplyFromConfig(
@@ -87,6 +88,7 @@ describe("RawBody directive parsing", () => {
From: "+1222",
To: "+1222",
ChatType: "group",
CommandAuthorized: true,
};
const res = await getReplyFromConfig(
@@ -123,6 +125,7 @@ describe("RawBody directive parsing", () => {
From: "+1222",
To: "+1222",
ChatType: "group",
CommandAuthorized: true,
};
const res = await getReplyFromConfig(
@@ -160,6 +163,7 @@ describe("RawBody directive parsing", () => {
Provider: "whatsapp",
Surface: "whatsapp",
SenderE164: "+1222",
CommandAuthorized: true,
};
const res = await getReplyFromConfig(
@@ -207,6 +211,7 @@ describe("RawBody directive parsing", () => {
From: "+1222",
To: "+1222",
ChatType: "group",
CommandAuthorized: true,
};
const res = await getReplyFromConfig(

View File

@@ -105,6 +105,7 @@ describe("trigger handling", () => {
ChatType: "group",
Provider: "whatsapp",
SenderE164: "+999",
CommandAuthorized: true,
},
{},
cfg,
@@ -179,6 +180,7 @@ describe("trigger handling", () => {
Body: "/new",
From: "+1003",
To: "+2000",
CommandAuthorized: true,
},
{},
{

View File

@@ -123,6 +123,7 @@ describe("trigger handling", () => {
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1000",
CommandAuthorized: true,
},
{},
cfg,

View File

@@ -132,6 +132,7 @@ describe("trigger handling", () => {
To: "whatsapp:+2000",
Provider: "whatsapp",
SenderE164: "+1000",
CommandAuthorized: true,
ChatType: "group",
WasMentioned: false,
},
@@ -175,6 +176,7 @@ describe("trigger handling", () => {
To: "whatsapp:+2000",
Provider: "whatsapp",
SenderE164: "+1000",
CommandAuthorized: true,
ChatType: "group",
WasMentioned: true,
},
@@ -218,6 +220,7 @@ describe("trigger handling", () => {
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1000",
CommandAuthorized: true,
},
{},
cfg,

View File

@@ -116,6 +116,7 @@ describe("trigger handling", () => {
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1000",
CommandAuthorized: true,
},
{},
makeCfg(home),
@@ -138,6 +139,7 @@ describe("trigger handling", () => {
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1000",
CommandAuthorized: true,
},
{
onBlockReply: async (payload) => {
@@ -162,6 +164,7 @@ describe("trigger handling", () => {
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1000",
CommandAuthorized: true,
},
{
onBlockReply: async (payload) => {
@@ -193,6 +196,7 @@ describe("trigger handling", () => {
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1002",
CommandAuthorized: true,
},
{
onBlockReply: async (payload) => {
@@ -217,6 +221,7 @@ describe("trigger handling", () => {
Body: "[Dec 5 10:00] stop",
From: "+1000",
To: "+2000",
CommandAuthorized: true,
},
{},
makeCfg(home),
@@ -233,6 +238,7 @@ describe("trigger handling", () => {
Body: "/stop",
From: "+1003",
To: "+2000",
CommandAuthorized: true,
},
{},
makeCfg(home),

View File

@@ -108,6 +108,7 @@ describe("trigger handling", () => {
Body: "please /commands now",
From: "+1002",
To: "+2000",
CommandAuthorized: true,
},
{
onBlockReply: async (payload) => {
@@ -141,6 +142,7 @@ describe("trigger handling", () => {
From: "+1002",
To: "+2000",
SenderId: "12345",
CommandAuthorized: true,
},
{
onBlockReply: async (payload) => {

View File

@@ -161,6 +161,7 @@ describe("trigger handling", () => {
SenderName: "Peter Steinberger",
SenderUsername: "steipete",
SenderTag: "steipete",
CommandAuthorized: true,
},
{},
cfg,

View File

@@ -209,6 +209,7 @@ describe("trigger handling", () => {
ChatType: "group",
Provider: "whatsapp",
SenderE164: "+2000",
CommandAuthorized: true,
},
{},
cfg,

View File

@@ -184,6 +184,7 @@ describe("trigger handling", () => {
Body: "/help",
From: "+1002",
To: "+2000",
CommandAuthorized: true,
},
{},
makeCfg(home),
@@ -218,6 +219,7 @@ describe("trigger handling", () => {
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1000",
CommandAuthorized: true,
},
{},
cfg,

View File

@@ -146,6 +146,7 @@ describe("trigger handling", () => {
To: "+2000",
Provider: "whatsapp",
SenderE164: "+1002",
CommandAuthorized: true,
},
{},
cfg,
@@ -176,6 +177,7 @@ describe("trigger handling", () => {
Provider: "whatsapp",
Surface: "whatsapp",
SenderE164: "+1002",
CommandAuthorized: true,
},
{
onBlockReply: async (payload) => {
@@ -208,6 +210,7 @@ describe("trigger handling", () => {
Body: "please /help now",
From: "+1002",
To: "+2000",
CommandAuthorized: true,
},
{
onBlockReply: async (payload) => {

View File

@@ -117,6 +117,7 @@ describe("trigger handling", () => {
Body: "/compact focus on decisions",
From: "+1003",
To: "+2000",
CommandAuthorized: true,
},
{},
{

View File

@@ -109,6 +109,7 @@ describe("trigger handling", () => {
Body: "/reset",
From: "+1003",
To: "+2000",
CommandAuthorized: true,
},
{},
{
@@ -173,6 +174,7 @@ describe("trigger handling", () => {
Body: "/reset",
From: "+1003",
To: "+2000",
CommandAuthorized: true,
},
{},
{

View File

@@ -106,6 +106,7 @@ describe("trigger handling", () => {
Provider: "telegram",
Surface: "telegram",
SessionKey: "telegram:slash:111",
CommandAuthorized: true,
},
{},
cfg,
@@ -137,6 +138,7 @@ describe("trigger handling", () => {
Provider: "telegram",
Surface: "telegram",
SessionKey: "telegram:slash:111",
CommandAuthorized: true,
},
{},
cfg,
@@ -156,6 +158,7 @@ describe("trigger handling", () => {
Body: " [Dec 5] /restart",
From: "+1001",
To: "+2000",
CommandAuthorized: true,
},
{},
makeCfg(home),
@@ -173,6 +176,7 @@ describe("trigger handling", () => {
Body: "/restart",
From: "+1001",
To: "+2000",
CommandAuthorized: true,
},
{},
cfg,
@@ -189,6 +193,7 @@ describe("trigger handling", () => {
Body: "/status",
From: "+1002",
To: "+2000",
CommandAuthorized: true,
},
{},
makeCfg(home),
@@ -205,6 +210,7 @@ describe("trigger handling", () => {
Body: "/usage",
From: "+1002",
To: "+2000",
CommandAuthorized: true,
},
{},
makeCfg(home),

View File

@@ -107,6 +107,7 @@ describe("trigger handling", () => {
Provider: "telegram",
Surface: "telegram",
SessionKey: "telegram:slash:111",
CommandAuthorized: true,
},
{},
cfg,
@@ -137,6 +138,7 @@ describe("trigger handling", () => {
Provider: "telegram",
Surface: "telegram",
SessionKey: "telegram:slash:111",
CommandAuthorized: true,
},
{},
cfg,
@@ -169,6 +171,7 @@ describe("trigger handling", () => {
Provider: "telegram",
Surface: "telegram",
SessionKey: sessionKey,
CommandAuthorized: true,
},
{},
cfg,
@@ -197,6 +200,7 @@ describe("trigger handling", () => {
Provider: "telegram",
Surface: "telegram",
SessionKey: sessionKey,
CommandAuthorized: true,
},
{},
cfg,
@@ -226,6 +230,7 @@ describe("trigger handling", () => {
Provider: "telegram",
Surface: "telegram",
SessionKey: sessionKey,
CommandAuthorized: true,
},
{},
cfg,
@@ -256,6 +261,7 @@ describe("trigger handling", () => {
Provider: "telegram",
Surface: "telegram",
SessionKey: sessionKey,
CommandAuthorized: true,
},
{},
cfg,
@@ -285,6 +291,7 @@ describe("trigger handling", () => {
Provider: "telegram",
Surface: "telegram",
SessionKey: sessionKey,
CommandAuthorized: true,
},
{},
cfg,

View File

@@ -70,6 +70,7 @@ describe("abort detection", () => {
ctx: {
CommandBody: "/stop",
RawBody: "/stop",
CommandAuthorized: true,
SessionKey: "telegram:123",
Provider: "telegram",
Surface: "telegram",
@@ -132,6 +133,7 @@ describe("abort detection", () => {
ctx: {
CommandBody: "/stop",
RawBody: "/stop",
CommandAuthorized: true,
SessionKey: sessionKey,
Provider: "telegram",
Surface: "telegram",
@@ -188,6 +190,7 @@ describe("abort detection", () => {
ctx: {
CommandBody: "/stop",
RawBody: "/stop",
CommandAuthorized: true,
SessionKey: sessionKey,
Provider: "telegram",
Surface: "telegram",

View File

@@ -132,7 +132,7 @@ export async function tryFastAbortFromMessage(params: {
const abortRequested = normalized === "/stop" || isAbortTrigger(stripped);
if (!abortRequested) return { handled: false, aborted: false };
const commandAuthorized = ctx.CommandAuthorized ?? true;
const commandAuthorized = ctx.CommandAuthorized ?? false;
const auth = resolveCommandAuthorization({
ctx,
cfg,

View File

@@ -84,7 +84,7 @@ export async function getReplyFromConfig(
activeModel: { provider, model },
});
const commandAuthorized = ctx.CommandAuthorized ?? true;
const commandAuthorized = ctx.CommandAuthorized ?? false;
resolveCommandAuthorization({
ctx,
cfg,

View File

@@ -18,6 +18,14 @@ describe("gateway ws log helpers", () => {
expect(formatForLog(obj)).toBe("Oops: failed: code=E1");
});
test("formatForLog redacts obvious secrets", () => {
const token = "sk-abcdefghijklmnopqrstuvwxyz123456";
const out = formatForLog({ token });
expect(out).toContain("token");
expect(out).not.toContain(token);
expect(out).toContain("…");
});
test("summarizeAgentEventForWsLog extracts useful fields", () => {
const summary = summarizeAgentEventForWsLog({
runId: "12345678-1234-1234-1234-123456789abc",

View File

@@ -1,10 +1,15 @@
import chalk from "chalk";
import { isVerbose } from "../globals.js";
import { getDefaultRedactPatterns, redactSensitiveText } from "../logging/redact.js";
import { shouldLogSubsystemToConsole } from "../logging.js";
import { DEFAULT_WS_SLOW_MS, getGatewayWsLogStyle } from "./ws-logging.js";
const LOG_VALUE_LIMIT = 240;
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const WS_LOG_REDACT_OPTIONS = {
mode: "tools" as const,
patterns: getDefaultRedactPatterns(),
};
type WsInflightEntry = {
ts: number;
@@ -61,7 +66,8 @@ export function formatForLog(value: unknown): string {
? String(value)
: JSON.stringify(value);
if (!str) return "";
return str.length > LOG_VALUE_LIMIT ? `${str.slice(0, LOG_VALUE_LIMIT)}...` : str;
const redacted = redactSensitiveText(str, WS_LOG_REDACT_OPTIONS);
return redacted.length > LOG_VALUE_LIMIT ? `${redacted.slice(0, LOG_VALUE_LIMIT)}...` : redacted;
} catch {
return String(value);
}

View File

@@ -269,6 +269,52 @@ describe("security audit", () => {
}
});
it("does not flag Discord slash commands when dm.allowFrom includes a Discord snowflake id", async () => {
const prevStateDir = process.env.CLAWDBOT_STATE_DIR;
const tmp = await fs.mkdtemp(
path.join(os.tmpdir(), "clawdbot-security-audit-discord-allowfrom-snowflake-"),
);
process.env.CLAWDBOT_STATE_DIR = tmp;
await fs.mkdir(path.join(tmp, "credentials"), { recursive: true, mode: 0o700 });
try {
const cfg: ClawdbotConfig = {
channels: {
discord: {
enabled: true,
token: "t",
dm: { allowFrom: ["387380367612706819"] },
groupPolicy: "allowlist",
guilds: {
"123": {
channels: {
general: { allow: true },
},
},
},
},
},
};
const res = await runSecurityAudit({
config: cfg,
includeFilesystem: false,
includeChannelSecurity: true,
plugins: [discordPlugin],
});
expect(res.findings).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
checkId: "channels.discord.commands.native.no_allowlists",
}),
]),
);
} finally {
if (prevStateDir == null) delete process.env.CLAWDBOT_STATE_DIR;
else process.env.CLAWDBOT_STATE_DIR = prevStateDir;
}
});
it("flags Discord slash commands when access-group enforcement is disabled and no users allowlist exists", async () => {
const prevStateDir = process.env.CLAWDBOT_STATE_DIR;
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-security-audit-discord-open-"));

View File

@@ -16,7 +16,7 @@ import {
import { dispatchReplyWithBufferedBlockDispatcher } from "../../../auto-reply/reply/provider-dispatcher.js";
import type { getReplyFromConfig } from "../../../auto-reply/reply.js";
import type { ReplyPayload } from "../../../auto-reply/types.js";
import { isControlCommandMessage } from "../../../auto-reply/command-detection.js";
import { hasInlineCommandTokens, isControlCommandMessage } from "../../../auto-reply/command-detection.js";
import { finalizeInboundContext } from "../../../auto-reply/reply/inbound-context.js";
import { toLocationContext } from "../../../channels/location.js";
import type { loadConfig } from "../../../config/config.js";
@@ -229,7 +229,9 @@ export async function processMessage(params: {
const textLimit = params.maxMediaTextChunkLimit ?? resolveTextChunkLimit(params.cfg, "whatsapp");
let didLogHeartbeatStrip = false;
let didSendReply = false;
const commandAuthorized = isControlCommandMessage(params.msg.body, params.cfg)
const shouldComputeCommandAuthorized =
isControlCommandMessage(params.msg.body, params.cfg) || hasInlineCommandTokens(params.msg.body);
const commandAuthorized = shouldComputeCommandAuthorized
? await resolveWhatsAppCommandAuthorized({ cfg: params.cfg, msg: params.msg })
: undefined;
const configuredResponsePrefix = params.cfg.messages?.responsePrefix;