fix: harden whatsapp command auth

This commit is contained in:
Peter Steinberger
2026-01-15 07:53:12 +00:00
parent 8c4b8f2c38
commit 8dacafce7f
7 changed files with 286 additions and 21 deletions

View File

@@ -169,6 +169,144 @@ describe("web auto-reply", () => {
expect(payload.Body).toContain("[from: Bob (+222)]");
});
it("bypasses mention gating for owner /new in group chats", async () => {
const sendMedia = vi.fn();
const reply = vi.fn().mockResolvedValue(undefined);
const sendComposing = vi.fn();
const resolver = vi.fn().mockResolvedValue({ text: "ok" });
let capturedOnMessage:
| ((msg: import("./inbound.js").WebInboundMessage) => Promise<void>)
| undefined;
const listenerFactory = async (opts: {
onMessage: (msg: import("./inbound.js").WebInboundMessage) => Promise<void>;
}) => {
capturedOnMessage = opts.onMessage;
return { close: vi.fn() };
};
setLoadConfigMock(() => ({
channels: {
whatsapp: {
allowFrom: ["+111"],
},
},
}));
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
body: "/new",
from: "123@g.us",
conversationId: "123@g.us",
chatId: "123@g.us",
chatType: "group",
to: "+2",
id: "g-new",
senderE164: "+111",
senderName: "Owner",
selfE164: "+999",
sendComposing,
reply,
sendMedia,
});
expect(resolver).toHaveBeenCalledTimes(1);
});
it("does not bypass mention gating for non-owner /new in group chats", async () => {
const sendMedia = vi.fn();
const reply = vi.fn().mockResolvedValue(undefined);
const sendComposing = vi.fn();
const resolver = vi.fn().mockResolvedValue({ text: "ok" });
let capturedOnMessage:
| ((msg: import("./inbound.js").WebInboundMessage) => Promise<void>)
| undefined;
const listenerFactory = async (opts: {
onMessage: (msg: import("./inbound.js").WebInboundMessage) => Promise<void>;
}) => {
capturedOnMessage = opts.onMessage;
return { close: vi.fn() };
};
setLoadConfigMock(() => ({
channels: {
whatsapp: {
allowFrom: ["+999"],
},
},
}));
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
body: "/new",
from: "123@g.us",
conversationId: "123@g.us",
chatId: "123@g.us",
chatType: "group",
to: "+2",
id: "g-new-unauth",
senderE164: "+111",
senderName: "NotOwner",
selfE164: "+999",
sendComposing,
reply,
sendMedia,
});
expect(resolver).not.toHaveBeenCalled();
});
it("bypasses mention gating for owner /status in group chats", async () => {
const sendMedia = vi.fn();
const reply = vi.fn().mockResolvedValue(undefined);
const sendComposing = vi.fn();
const resolver = vi.fn().mockResolvedValue({ text: "ok" });
let capturedOnMessage:
| ((msg: import("./inbound.js").WebInboundMessage) => Promise<void>)
| undefined;
const listenerFactory = async (opts: {
onMessage: (msg: import("./inbound.js").WebInboundMessage) => Promise<void>;
}) => {
capturedOnMessage = opts.onMessage;
return { close: vi.fn() };
};
setLoadConfigMock(() => ({
channels: {
whatsapp: {
allowFrom: ["+111"],
},
},
}));
await monitorWebChannel(false, listenerFactory, false, resolver);
expect(capturedOnMessage).toBeDefined();
await capturedOnMessage?.({
body: "/status",
from: "123@g.us",
conversationId: "123@g.us",
chatId: "123@g.us",
chatType: "group",
to: "+2",
id: "g-status",
senderE164: "+111",
senderName: "Owner",
selfE164: "+999",
sendComposing,
reply,
sendMedia,
});
expect(resolver).toHaveBeenCalledTimes(1);
});
it("passes conversation id through as From for group replies", async () => {
const sendMedia = vi.fn();
const reply = vi.fn().mockResolvedValue(undefined);

View File

@@ -1,10 +1,11 @@
import { hasControlCommand } from "../../../auto-reply/command-detection.js";
import { parseActivationCommand } from "../../../auto-reply/group-activation.js";
import type { loadConfig } from "../../../config/config.js";
import { normalizeE164 } from "../../../utils.js";
import type { MentionConfig } from "../mentions.js";
import { buildMentionConfig, debugMention, resolveOwnerList } from "../mentions.js";
import type { WebInboundMsg } from "../types.js";
import { isStatusCommand, stripMentionsForCommand } from "./commands.js";
import { stripMentionsForCommand } from "./commands.js";
import { resolveGroupActivationFor, resolveGroupPolicyFor } from "./group-activation.js";
import { noteGroupMember } from "./group-members.js";
@@ -59,8 +60,7 @@ export function applyGroupGating(params: {
);
const activationCommand = parseActivationCommand(commandBody);
const owner = isOwnerSender(params.baseMentionConfig, params.msg);
const statusCommand = isStatusCommand(commandBody);
const shouldBypassMention = owner && (activationCommand.hasCommand || statusCommand);
const shouldBypassMention = owner && hasControlCommand(commandBody, params.cfg);
if (activationCommand.hasCommand && !owner) {
params.logVerbose(`Ignoring /activation from non-owner in group ${params.conversationId}`);