diff --git a/CHANGELOG.md b/CHANGELOG.md index bf4bbaf24..e0b04b119 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ - Docs: clarify Slack manifest scopes (current vs optional) with references. Thanks @jarvis-medmatic for PR #235. - Control UI: avoid Slack config ReferenceError by reading slack config snapshots. Thanks @sreekaransrinath for PR #249. - Telegram: honor routing.groupChat.mentionPatterns for group mention gating. Thanks @regenrek for PR #242. +- Auto-reply: block unauthorized `/reset` and infer WhatsApp senders from E.164 inputs. ### Maintenance - Deps: bump pi-* stack, Slack SDK, discord-api-types, file-type, zod, and Biome. diff --git a/src/auto-reply/command-auth.ts b/src/auto-reply/command-auth.ts index d48141802..7599c7390 100644 --- a/src/auto-reply/command-auth.ts +++ b/src/auto-reply/command-auth.ts @@ -18,16 +18,23 @@ export function resolveCommandAuthorization(params: { }): CommandAuthorization { const { ctx, cfg, commandAuthorized } = params; const surface = (ctx.Surface ?? "").trim().toLowerCase(); - const isWhatsAppSurface = - surface === "whatsapp" || + const from = (ctx.From ?? "").replace(/^whatsapp:/, ""); + const to = (ctx.To ?? "").replace(/^whatsapp:/, ""); + const hasWhatsappPrefix = (ctx.From ?? "").startsWith("whatsapp:") || (ctx.To ?? "").startsWith("whatsapp:"); + const looksLikeE164 = (value: string) => + Boolean(value && /^\+?\d{3,}$/.test(value.replace(/[^\d+]/g, ""))); + const inferWhatsApp = + !surface && + Boolean(cfg.whatsapp?.allowFrom?.length) && + (looksLikeE164(from) || looksLikeE164(to)); + const isWhatsAppSurface = + surface === "whatsapp" || hasWhatsappPrefix || inferWhatsApp; const configuredAllowFrom = isWhatsAppSurface ? cfg.whatsapp?.allowFrom : undefined; - const from = (ctx.From ?? "").replace(/^whatsapp:/, ""); - const to = (ctx.To ?? "").replace(/^whatsapp:/, ""); const allowFromList = configuredAllowFrom?.filter((entry) => entry?.trim()) ?? []; const allowAll = @@ -35,7 +42,9 @@ export function resolveCommandAuthorization(params: { allowFromList.length === 0 || allowFromList.some((entry) => entry.trim() === "*"); - const senderE164 = normalizeE164(ctx.SenderE164 ?? ""); + const senderE164 = normalizeE164( + ctx.SenderE164 ?? (isWhatsAppSurface ? from : ""), + ); const ownerCandidates = isWhatsAppSurface && !allowAll ? allowFromList.filter((entry) => entry !== "*") diff --git a/src/auto-reply/reply/commands.ts b/src/auto-reply/reply/commands.ts index dcfe6d769..21b65a91c 100644 --- a/src/auto-reply/reply/commands.ts +++ b/src/auto-reply/reply/commands.ts @@ -173,6 +173,7 @@ export async function handleCommands(params: { shouldContinue: boolean; }> { const { + ctx, cfg, command, directives, @@ -193,6 +194,18 @@ export async function handleCommands(params: { isGroup, } = params; + const resetRequested = + command.commandBodyNormalized === "/reset" || + command.commandBodyNormalized === "reset" || + command.commandBodyNormalized === "/new" || + command.commandBodyNormalized === "new"; + if (resetRequested && !command.isAuthorizedSender) { + logVerbose( + `Ignoring /reset from unauthorized sender: ${command.senderE164 || ""}`, + ); + return { shouldContinue: false }; + } + const activationCommand = parseActivationCommand( command.commandBodyNormalized, );