From 0bd9f0d4acb691915e865c00c302db17b879d04b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 22 Feb 2026 00:00:23 +0100 Subject: [PATCH] fix: enforce strict allowlist across pairing stores (#23017) --- .../bluebubbles/src/monitor-processing.ts | 2 ++ extensions/feishu/src/bot.ts | 4 +++- extensions/googlechat/src/monitor.ts | 2 +- extensions/irc/src/inbound.ts | 5 ++++- .../matrix/src/matrix/monitor/handler.ts | 7 +++--- .../mattermost/src/mattermost/monitor.ts | 13 ++++++++--- .../src/monitor-handler/message-handler.ts | 9 ++++---- extensions/nextcloud-talk/src/inbound.ts | 5 ++++- src/channels/allow-from.test.ts | 20 +++++++++++++++++ src/channels/allow-from.ts | 4 +++- src/discord/monitor/agent-components.ts | 3 ++- .../monitor/message-handler.preflight.ts | 3 ++- src/discord/monitor/monitor.test.ts | 7 +++--- src/discord/monitor/native-command.ts | 3 ++- src/imessage/monitor/inbound-processing.ts | 3 ++- src/line/bot-access.ts | 1 + src/line/bot-handlers.ts | 4 +++- src/plugin-sdk/command-auth.ts | 4 +++- src/security/dm-policy-shared.test.ts | 22 +++++++++++++++++++ src/security/dm-policy-shared.ts | 10 ++++++--- src/signal/monitor/event-handler.ts | 5 ++++- src/slack/monitor/auth.ts | 3 ++- src/slack/monitor/slash.ts | 5 ++++- src/telegram/bot-access.ts | 1 + src/telegram/bot-handlers.ts | 5 ++++- src/telegram/bot-message-context.ts | 3 ++- src/telegram/bot-native-commands.ts | 2 ++ src/telegram/bot/helpers.ts | 2 ++ src/web/auto-reply/monitor/process-message.ts | 19 +++++++++------- src/web/inbound/access-control.test.ts | 22 +++++++++++++++++++ src/web/inbound/access-control.ts | 9 ++++---- 31 files changed, 162 insertions(+), 45 deletions(-) diff --git a/extensions/bluebubbles/src/monitor-processing.ts b/extensions/bluebubbles/src/monitor-processing.ts index 9b61fc9ec58..77457c4f5ef 100644 --- a/extensions/bluebubbles/src/monitor-processing.ts +++ b/extensions/bluebubbles/src/monitor-processing.ts @@ -332,6 +332,7 @@ export async function processMessage( allowFrom: account.config.allowFrom, groupAllowFrom: account.config.groupAllowFrom, storeAllowFrom, + dmPolicy, }); const groupAllowEntry = formatGroupAllowlistEntry({ chatGuid: message.chatGuid, @@ -1107,6 +1108,7 @@ export async function processReaction( allowFrom: account.config.allowFrom, groupAllowFrom: account.config.groupAllowFrom, storeAllowFrom, + dmPolicy, }); const accessDecision = resolveDmGroupAccessDecision({ isGroup: reaction.isGroup, diff --git a/extensions/feishu/src/bot.ts b/extensions/feishu/src/bot.ts index 9e1ea5934ac..bee417c5741 100644 --- a/extensions/feishu/src/bot.ts +++ b/extensions/feishu/src/bot.ts @@ -630,7 +630,9 @@ export async function handleFeishuMessage(params: { cfg, ); const storeAllowFrom = - !isGroup && (dmPolicy !== "open" || shouldComputeCommandAuthorized) + !isGroup && + dmPolicy !== "allowlist" && + (dmPolicy !== "open" || shouldComputeCommandAuthorized) ? await core.channel.pairing.readAllowFromStore("feishu").catch(() => []) : []; const effectiveDmAllowFrom = [...configAllowFrom, ...storeAllowFrom]; diff --git a/extensions/googlechat/src/monitor.ts b/extensions/googlechat/src/monitor.ts index 9cdcbc070fb..cee54005886 100644 --- a/extensions/googlechat/src/monitor.ts +++ b/extensions/googlechat/src/monitor.ts @@ -485,7 +485,7 @@ async function processMessageWithPipeline(params: { const configAllowFrom = (account.config.dm?.allowFrom ?? []).map((v) => String(v)); const shouldComputeAuth = core.channel.commands.shouldComputeCommandAuthorized(rawBody, config); const storeAllowFrom = - !isGroup && (dmPolicy !== "open" || shouldComputeAuth) + !isGroup && dmPolicy !== "allowlist" && (dmPolicy !== "open" || shouldComputeAuth) ? await core.channel.pairing.readAllowFromStore("googlechat").catch(() => []) : []; const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom]; diff --git a/extensions/irc/src/inbound.ts b/extensions/irc/src/inbound.ts index 01c69285e2d..abd523ed17c 100644 --- a/extensions/irc/src/inbound.ts +++ b/extensions/irc/src/inbound.ts @@ -89,7 +89,10 @@ export async function handleIrcInbound(params: { const configAllowFrom = normalizeIrcAllowlist(account.config.allowFrom); const configGroupAllowFrom = normalizeIrcAllowlist(account.config.groupAllowFrom); - const storeAllowFrom = await core.channel.pairing.readAllowFromStore(CHANNEL_ID).catch(() => []); + const storeAllowFrom = + dmPolicy === "allowlist" + ? [] + : await core.channel.pairing.readAllowFromStore(CHANNEL_ID).catch(() => []); const storeAllowList = normalizeIrcAllowlist(storeAllowFrom); const groupMatch = resolveIrcGroupMatch({ diff --git a/extensions/matrix/src/matrix/monitor/handler.ts b/extensions/matrix/src/matrix/monitor/handler.ts index ae8e8643020..d884879001e 100644 --- a/extensions/matrix/src/matrix/monitor/handler.ts +++ b/extensions/matrix/src/matrix/monitor/handler.ts @@ -218,9 +218,10 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam } const senderName = await getMemberDisplayName(roomId, senderId); - const storeAllowFrom = await core.channel.pairing - .readAllowFromStore("matrix") - .catch(() => []); + const storeAllowFrom = + dmPolicy === "allowlist" + ? [] + : await core.channel.pairing.readAllowFromStore("matrix").catch(() => []); const effectiveAllowFrom = normalizeMatrixAllowList([...allowFrom, ...storeAllowFrom]); const groupAllowFrom = cfg.channels?.matrix?.groupAllowFrom ?? []; const effectiveGroupAllowFrom = normalizeMatrixAllowList(groupAllowFrom); diff --git a/extensions/mattermost/src/mattermost/monitor.ts b/extensions/mattermost/src/mattermost/monitor.ts index 5cee9fb47e9..b2c921b155d 100644 --- a/extensions/mattermost/src/mattermost/monitor.ts +++ b/extensions/mattermost/src/mattermost/monitor.ts @@ -380,7 +380,9 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} const configAllowFrom = normalizeAllowList(account.config.allowFrom ?? []); const configGroupAllowFrom = normalizeAllowList(account.config.groupAllowFrom ?? []); const storeAllowFrom = normalizeAllowList( - await core.channel.pairing.readAllowFromStore("mattermost").catch(() => []), + dmPolicy === "allowlist" + ? [] + : await core.channel.pairing.readAllowFromStore("mattermost").catch(() => []), ); const effectiveAllowFrom = Array.from(new Set([...configAllowFrom, ...storeAllowFrom])); const effectiveGroupAllowFrom = Array.from( @@ -867,7 +869,9 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} if (dmPolicy !== "open") { const configAllowFrom = normalizeAllowList(account.config.allowFrom ?? []); const storeAllowFrom = normalizeAllowList( - await core.channel.pairing.readAllowFromStore("mattermost").catch(() => []), + dmPolicy === "allowlist" + ? [] + : await core.channel.pairing.readAllowFromStore("mattermost").catch(() => []), ); const effectiveAllowFrom = Array.from(new Set([...configAllowFrom, ...storeAllowFrom])); const allowed = isSenderAllowed({ @@ -890,10 +894,13 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {} return; } if (groupPolicy === "allowlist") { + const dmPolicyForStore = account.config.dmPolicy ?? "pairing"; const configAllowFrom = normalizeAllowList(account.config.allowFrom ?? []); const configGroupAllowFrom = normalizeAllowList(account.config.groupAllowFrom ?? []); const storeAllowFrom = normalizeAllowList( - await core.channel.pairing.readAllowFromStore("mattermost").catch(() => []), + dmPolicyForStore === "allowlist" + ? [] + : await core.channel.pairing.readAllowFromStore("mattermost").catch(() => []), ); const effectiveGroupAllowFrom = Array.from( new Set([ diff --git a/extensions/msteams/src/monitor-handler/message-handler.ts b/extensions/msteams/src/monitor-handler/message-handler.ts index ac3f20adf92..ae1f203a016 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.ts @@ -124,16 +124,17 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { const senderName = from.name ?? from.id; const senderId = from.aadObjectId ?? from.id; - const storedAllowFrom = await core.channel.pairing - .readAllowFromStore("msteams") - .catch(() => []); + const dmPolicy = msteamsCfg?.dmPolicy ?? "pairing"; + const storedAllowFrom = + dmPolicy === "allowlist" + ? [] + : await core.channel.pairing.readAllowFromStore("msteams").catch(() => []); const useAccessGroups = cfg.commands?.useAccessGroups !== false; // Check DM policy for direct messages. const dmAllowFrom = msteamsCfg?.allowFrom ?? []; const effectiveDmAllowFrom = [...dmAllowFrom.map((v) => String(v)), ...storedAllowFrom]; if (isDirectMessage && msteamsCfg) { - const dmPolicy = msteamsCfg.dmPolicy ?? "pairing"; const allowFrom = dmAllowFrom; if (dmPolicy === "disabled") { diff --git a/extensions/nextcloud-talk/src/inbound.ts b/extensions/nextcloud-talk/src/inbound.ts index 1971166d4e6..642e010b06d 100644 --- a/extensions/nextcloud-talk/src/inbound.ts +++ b/extensions/nextcloud-talk/src/inbound.ts @@ -93,7 +93,10 @@ export async function handleNextcloudTalkInbound(params: { const configAllowFrom = normalizeNextcloudTalkAllowlist(account.config.allowFrom); const configGroupAllowFrom = normalizeNextcloudTalkAllowlist(account.config.groupAllowFrom); - const storeAllowFrom = await core.channel.pairing.readAllowFromStore(CHANNEL_ID).catch(() => []); + const storeAllowFrom = + dmPolicy === "allowlist" + ? [] + : await core.channel.pairing.readAllowFromStore(CHANNEL_ID).catch(() => []); const storeAllowList = normalizeNextcloudTalkAllowlist(storeAllowFrom); const roomMatch = resolveNextcloudTalkRoomMatch({ diff --git a/src/channels/allow-from.test.ts b/src/channels/allow-from.test.ts index a802349a1a2..e4dc4aa1492 100644 --- a/src/channels/allow-from.test.ts +++ b/src/channels/allow-from.test.ts @@ -10,6 +10,26 @@ describe("mergeAllowFromSources", () => { }), ).toEqual(["line:user:abc", "123", "telegram:456"]); }); + + it("excludes pairing-store entries when dmPolicy is allowlist", () => { + expect( + mergeAllowFromSources({ + allowFrom: ["+1111"], + storeAllowFrom: ["+2222", "+3333"], + dmPolicy: "allowlist", + }), + ).toEqual(["+1111"]); + }); + + it("keeps pairing-store entries for non-allowlist policies", () => { + expect( + mergeAllowFromSources({ + allowFrom: ["+1111"], + storeAllowFrom: ["+2222"], + dmPolicy: "pairing", + }), + ).toEqual(["+1111", "+2222"]); + }); }); describe("firstDefined", () => { diff --git a/src/channels/allow-from.ts b/src/channels/allow-from.ts index 8ab2f65c11b..774912309bb 100644 --- a/src/channels/allow-from.ts +++ b/src/channels/allow-from.ts @@ -1,8 +1,10 @@ export function mergeAllowFromSources(params: { allowFrom?: Array; storeAllowFrom?: string[]; + dmPolicy?: string; }): string[] { - return [...(params.allowFrom ?? []), ...(params.storeAllowFrom ?? [])] + const storeEntries = params.dmPolicy === "allowlist" ? [] : (params.storeAllowFrom ?? []); + return [...(params.allowFrom ?? []), ...storeEntries] .map((value) => String(value).trim()) .filter(Boolean); } diff --git a/src/discord/monitor/agent-components.ts b/src/discord/monitor/agent-components.ts index ed0bb8824fe..4423e7796e6 100644 --- a/src/discord/monitor/agent-components.ts +++ b/src/discord/monitor/agent-components.ts @@ -464,7 +464,8 @@ async function ensureDmComponentAuthorized(params: { return true; } - const storeAllowFrom = await readChannelAllowFromStore("discord").catch(() => []); + const storeAllowFrom = + dmPolicy === "allowlist" ? [] : await readChannelAllowFromStore("discord").catch(() => []); const effectiveAllowFrom = [...(ctx.allowFrom ?? []), ...storeAllowFrom]; const allowList = normalizeDiscordAllowList(effectiveAllowFrom, ["discord:", "user:", "pk:"]); const allowMatch = allowList diff --git a/src/discord/monitor/message-handler.preflight.ts b/src/discord/monitor/message-handler.preflight.ts index 0d648aeb7ea..f343cb58328 100644 --- a/src/discord/monitor/message-handler.preflight.ts +++ b/src/discord/monitor/message-handler.preflight.ts @@ -178,7 +178,8 @@ export async function preflightDiscordMessage( return null; } if (dmPolicy !== "open") { - const storeAllowFrom = await readChannelAllowFromStore("discord").catch(() => []); + const storeAllowFrom = + dmPolicy === "allowlist" ? [] : await readChannelAllowFromStore("discord").catch(() => []); const effectiveAllowFrom = [...(params.allowFrom ?? []), ...storeAllowFrom]; const allowList = normalizeDiscordAllowList(effectiveAllowFrom, ["discord:", "user:", "pk:"]); const allowMatch = allowList diff --git a/src/discord/monitor/monitor.test.ts b/src/discord/monitor/monitor.test.ts index d9abf4103aa..46ab7d1e795 100644 --- a/src/discord/monitor/monitor.test.ts +++ b/src/discord/monitor/monitor.test.ts @@ -140,7 +140,7 @@ describe("agent components", () => { expect(enqueueSystemEventMock).not.toHaveBeenCalled(); }); - it("allows DM interactions when pairing store allowlist matches", async () => { + it("blocks DM interactions when only pairing store entries match in allowlist mode", async () => { readAllowFromStoreMock.mockResolvedValue(["123456789"]); const button = createAgentComponentButton({ cfg: createCfg(), @@ -152,8 +152,9 @@ describe("agent components", () => { await button.run(interaction, { componentId: "hello" } as ComponentData); expect(defer).toHaveBeenCalledWith({ ephemeral: true }); - expect(reply).toHaveBeenCalledWith({ content: "✓" }); - expect(enqueueSystemEventMock).toHaveBeenCalled(); + expect(reply).toHaveBeenCalledWith({ content: "You are not authorized to use this button." }); + expect(enqueueSystemEventMock).not.toHaveBeenCalled(); + expect(readAllowFromStoreMock).not.toHaveBeenCalled(); }); it("matches tag-based allowlist entries for DM select menus", async () => { diff --git a/src/discord/monitor/native-command.ts b/src/discord/monitor/native-command.ts index 7391d36cba2..cc45838c3c9 100644 --- a/src/discord/monitor/native-command.ts +++ b/src/discord/monitor/native-command.ts @@ -1349,7 +1349,8 @@ async function dispatchDiscordCommandInteraction(params: { return; } if (dmPolicy !== "open") { - const storeAllowFrom = await readChannelAllowFromStore("discord").catch(() => []); + const storeAllowFrom = + dmPolicy === "allowlist" ? [] : await readChannelAllowFromStore("discord").catch(() => []); const effectiveAllowFrom = [ ...(discordConfig?.allowFrom ?? discordConfig?.dm?.allowFrom ?? []), ...storeAllowFrom, diff --git a/src/imessage/monitor/inbound-processing.ts b/src/imessage/monitor/inbound-processing.ts index 8ed2bbb51ec..5f4757bf542 100644 --- a/src/imessage/monitor/inbound-processing.ts +++ b/src/imessage/monitor/inbound-processing.ts @@ -138,7 +138,8 @@ export function resolveIMessageInboundDecision(params: { } const groupId = isGroup ? groupIdCandidate : undefined; - const effectiveDmAllowFrom = Array.from(new Set([...params.allowFrom, ...params.storeAllowFrom])) + const storeAllowFrom = params.dmPolicy === "allowlist" ? [] : params.storeAllowFrom; + const effectiveDmAllowFrom = Array.from(new Set([...params.allowFrom, ...storeAllowFrom])) .map((v) => String(v).trim()) .filter(Boolean); // Keep DM pairing-store authorization scoped to DMs; group access must come from explicit group allowlist config. diff --git a/src/line/bot-access.ts b/src/line/bot-access.ts index 2c4094406fc..fa7d87ae48c 100644 --- a/src/line/bot-access.ts +++ b/src/line/bot-access.ts @@ -30,6 +30,7 @@ export const normalizeAllowFrom = (list?: Array): NormalizedAll export const normalizeAllowFromWithStore = (params: { allowFrom?: Array; storeAllowFrom?: string[]; + dmPolicy?: string; }): NormalizedAllowFrom => normalizeAllowFrom(mergeAllowFromSources(params)); export const isSenderAllowed = (params: { diff --git a/src/line/bot-handlers.ts b/src/line/bot-handlers.ts index 45914996801..206a4d185cb 100644 --- a/src/line/bot-handlers.ts +++ b/src/line/bot-handlers.ts @@ -109,11 +109,13 @@ async function shouldProcessLineEvent( const { cfg, account } = context; const { userId, groupId, roomId, isGroup } = getLineSourceInfo(event.source); const senderId = userId ?? ""; + const dmPolicy = account.config.dmPolicy ?? "pairing"; const storeAllowFrom = await readChannelAllowFromStore("line").catch(() => []); const effectiveDmAllow = normalizeAllowFromWithStore({ allowFrom: account.config.allowFrom, storeAllowFrom, + dmPolicy, }); const groupConfig = resolveLineGroupConfig({ config: account.config, groupId, roomId }); const groupAllowOverride = groupConfig?.allowFrom; @@ -128,8 +130,8 @@ async function shouldProcessLineEvent( const effectiveGroupAllow = normalizeAllowFromWithStore({ allowFrom: groupAllowFrom, storeAllowFrom, + dmPolicy, }); - const dmPolicy = account.config.dmPolicy ?? "pairing"; const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy; const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist"; diff --git a/src/plugin-sdk/command-auth.ts b/src/plugin-sdk/command-auth.ts index 135846f6378..287f1398da4 100644 --- a/src/plugin-sdk/command-auth.ts +++ b/src/plugin-sdk/command-auth.ts @@ -26,7 +26,9 @@ export async function resolveSenderCommandAuthorization( }> { const shouldComputeAuth = params.shouldComputeCommandAuthorized(params.rawBody, params.cfg); const storeAllowFrom = - !params.isGroup && (params.dmPolicy !== "open" || shouldComputeAuth) + !params.isGroup && + params.dmPolicy !== "allowlist" && + (params.dmPolicy !== "open" || shouldComputeAuth) ? await params.readAllowFromStore().catch(() => []) : []; const effectiveAllowFrom = [...params.configuredAllowFrom, ...storeAllowFrom]; diff --git a/src/security/dm-policy-shared.test.ts b/src/security/dm-policy-shared.test.ts index bedc1ac67b0..d65d6a79188 100644 --- a/src/security/dm-policy-shared.test.ts +++ b/src/security/dm-policy-shared.test.ts @@ -53,6 +53,28 @@ describe("security/dm-policy-shared", () => { expect(lists.effectiveGroupAllowFrom).toEqual(["owner", "owner2"]); }); + it("excludes storeAllowFrom when dmPolicy is allowlist", () => { + const lists = resolveEffectiveAllowFromLists({ + allowFrom: ["+1111"], + groupAllowFrom: ["group:abc"], + storeAllowFrom: ["+2222", "+3333"], + dmPolicy: "allowlist", + }); + expect(lists.effectiveAllowFrom).toEqual(["+1111"]); + expect(lists.effectiveGroupAllowFrom).toEqual(["group:abc"]); + }); + + it("includes storeAllowFrom when dmPolicy is pairing", () => { + const lists = resolveEffectiveAllowFromLists({ + allowFrom: ["+1111"], + groupAllowFrom: [], + storeAllowFrom: ["+2222"], + dmPolicy: "pairing", + }); + expect(lists.effectiveAllowFrom).toEqual(["+1111", "+2222"]); + expect(lists.effectiveGroupAllowFrom).toEqual(["+1111", "+2222"]); + }); + const channels = [ "bluebubbles", "imessage", diff --git a/src/security/dm-policy-shared.ts b/src/security/dm-policy-shared.ts index 8e0d80306a1..ee07dfff3c7 100644 --- a/src/security/dm-policy-shared.ts +++ b/src/security/dm-policy-shared.ts @@ -6,6 +6,7 @@ export function resolveEffectiveAllowFromLists(params: { allowFrom?: Array | null; groupAllowFrom?: Array | null; storeAllowFrom?: Array | null; + dmPolicy?: string | null; }): { effectiveAllowFrom: string[]; effectiveGroupAllowFrom: string[]; @@ -16,9 +17,12 @@ export function resolveEffectiveAllowFromLists(params: { const configGroupAllowFrom = normalizeStringEntries( Array.isArray(params.groupAllowFrom) ? params.groupAllowFrom : undefined, ); - const storeAllowFrom = normalizeStringEntries( - Array.isArray(params.storeAllowFrom) ? params.storeAllowFrom : undefined, - ); + const storeAllowFrom = + params.dmPolicy === "allowlist" + ? [] + : normalizeStringEntries( + Array.isArray(params.storeAllowFrom) ? params.storeAllowFrom : undefined, + ); const effectiveAllowFrom = normalizeStringEntries([...configAllowFrom, ...storeAllowFrom]); const groupBase = configGroupAllowFrom.length > 0 ? configGroupAllowFrom : configAllowFrom; const effectiveGroupAllowFrom = normalizeStringEntries([...groupBase, ...storeAllowFrom]); diff --git a/src/signal/monitor/event-handler.ts b/src/signal/monitor/event-handler.ts index 71c81218524..8454de9d525 100644 --- a/src/signal/monitor/event-handler.ts +++ b/src/signal/monitor/event-handler.ts @@ -441,7 +441,10 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { const groupId = dataMessage.groupInfo?.groupId ?? undefined; const groupName = dataMessage.groupInfo?.groupName ?? undefined; const isGroup = Boolean(groupId); - const storeAllowFrom = await readChannelAllowFromStore("signal").catch(() => []); + const storeAllowFrom = + deps.dmPolicy === "allowlist" + ? [] + : await readChannelAllowFromStore("signal").catch(() => []); const effectiveDmAllow = [...deps.allowFrom, ...storeAllowFrom]; const effectiveGroupAllow = [...deps.groupAllowFrom, ...storeAllowFrom]; const dmAllowed = diff --git a/src/slack/monitor/auth.ts b/src/slack/monitor/auth.ts index 4fca101d26b..9b050f5a654 100644 --- a/src/slack/monitor/auth.ts +++ b/src/slack/monitor/auth.ts @@ -3,7 +3,8 @@ import { allowListMatches, normalizeAllowList, normalizeAllowListLower } from ". import type { SlackMonitorContext } from "./context.js"; export async function resolveSlackEffectiveAllowFrom(ctx: SlackMonitorContext) { - const storeAllowFrom = await readChannelAllowFromStore("slack").catch(() => []); + const storeAllowFrom = + ctx.dmPolicy === "allowlist" ? [] : await readChannelAllowFromStore("slack").catch(() => []); const allowFrom = normalizeAllowList([...ctx.allowFrom, ...storeAllowFrom]); const allowFromLower = normalizeAllowListLower(allowFrom); return { allowFrom, allowFromLower }; diff --git a/src/slack/monitor/slash.ts b/src/slack/monitor/slash.ts index a0651941bf5..bc379db5924 100644 --- a/src/slack/monitor/slash.ts +++ b/src/slack/monitor/slash.ts @@ -350,7 +350,10 @@ export async function registerSlackMonitorSlashCommands(params: { return; } - const storeAllowFrom = await readChannelAllowFromStore("slack").catch(() => []); + const storeAllowFrom = + ctx.dmPolicy === "allowlist" + ? [] + : await readChannelAllowFromStore("slack").catch(() => []); const effectiveAllowFrom = normalizeAllowList([...ctx.allowFrom, ...storeAllowFrom]); const effectiveAllowFromLower = normalizeAllowListLower(effectiveAllowFrom); diff --git a/src/telegram/bot-access.ts b/src/telegram/bot-access.ts index 73f1dbec57a..48ba43a64c2 100644 --- a/src/telegram/bot-access.ts +++ b/src/telegram/bot-access.ts @@ -56,6 +56,7 @@ export const normalizeAllowFrom = (list?: Array): NormalizedAll export const normalizeAllowFromWithStore = (params: { allowFrom?: Array; storeAllowFrom?: string[]; + dmPolicy?: string; }): NormalizedAllowFrom => normalizeAllowFrom(mergeAllowFromSources(params)); export const isSenderAllowed = (params: { diff --git a/src/telegram/bot-handlers.ts b/src/telegram/bot-handlers.ts index 1e51e0dbca5..6c31a059b0d 100644 --- a/src/telegram/bot-handlers.ts +++ b/src/telegram/bot-handlers.ts @@ -794,6 +794,7 @@ export const registerTelegramHandlers = ({ const groupAllowContext = await resolveTelegramGroupAllowFromContext({ chatId, accountId, + dmPolicy: telegramCfg.dmPolicy ?? "pairing", isForum, messageThreadId, groupAllowFrom, @@ -807,11 +808,12 @@ export const registerTelegramHandlers = ({ effectiveGroupAllow, hasGroupAllowOverride, } = groupAllowContext; + const dmPolicy = telegramCfg.dmPolicy ?? "pairing"; const effectiveDmAllow = normalizeAllowFromWithStore({ allowFrom: telegramCfg.allowFrom, storeAllowFrom, + dmPolicy, }); - const dmPolicy = telegramCfg.dmPolicy ?? "pairing"; const senderId = callback.from?.id ? String(callback.from.id) : ""; const senderUsername = callback.from?.username ?? ""; if ( @@ -1089,6 +1091,7 @@ export const registerTelegramHandlers = ({ const groupAllowContext = await resolveTelegramGroupAllowFromContext({ chatId: event.chatId, accountId, + dmPolicy: telegramCfg.dmPolicy ?? "pairing", isForum: event.isForum, messageThreadId: event.messageThreadId, groupAllowFrom, diff --git a/src/telegram/bot-message-context.ts b/src/telegram/bot-message-context.ts index 951b381d216..312f12f8efc 100644 --- a/src/telegram/bot-message-context.ts +++ b/src/telegram/bot-message-context.ts @@ -197,11 +197,12 @@ export const buildTelegramMessageContext = async ({ : null; const sessionKey = threadKeys?.sessionKey ?? baseSessionKey; const mentionRegexes = buildMentionRegexes(cfg, route.agentId); - const effectiveDmAllow = normalizeAllowFromWithStore({ allowFrom, storeAllowFrom }); + const effectiveDmAllow = normalizeAllowFromWithStore({ allowFrom, storeAllowFrom, dmPolicy }); const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom); const effectiveGroupAllow = normalizeAllowFromWithStore({ allowFrom: groupAllowOverride ?? groupAllowFrom, storeAllowFrom, + dmPolicy, }); const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined"; const senderId = msg.from?.id ? String(msg.from.id) : ""; diff --git a/src/telegram/bot-native-commands.ts b/src/telegram/bot-native-commands.ts index 1448d6c8183..424139c84d7 100644 --- a/src/telegram/bot-native-commands.ts +++ b/src/telegram/bot-native-commands.ts @@ -167,6 +167,7 @@ async function resolveTelegramCommandAuth(params: { const groupAllowContext = await resolveTelegramGroupAllowFromContext({ chatId, accountId, + dmPolicy: telegramCfg.dmPolicy ?? "pairing", isForum, messageThreadId, groupAllowFrom, @@ -251,6 +252,7 @@ async function resolveTelegramCommandAuth(params: { const dmAllow = normalizeAllowFromWithStore({ allowFrom: allowFrom, storeAllowFrom, + dmPolicy: telegramCfg.dmPolicy ?? "pairing", }); const senderAllowed = isSenderAllowed({ allow: dmAllow, diff --git a/src/telegram/bot/helpers.ts b/src/telegram/bot/helpers.ts index 79bc7f75dc2..d8e9560ce18 100644 --- a/src/telegram/bot/helpers.ts +++ b/src/telegram/bot/helpers.ts @@ -20,6 +20,7 @@ export type TelegramThreadSpec = { export async function resolveTelegramGroupAllowFromContext(params: { chatId: string | number; accountId?: string; + dmPolicy?: string; isForum?: boolean; messageThreadId?: number | null; groupAllowFrom?: Array; @@ -53,6 +54,7 @@ export async function resolveTelegramGroupAllowFromContext(params: { const effectiveGroupAllow = normalizeAllowFromWithStore({ allowFrom: groupAllowOverride ?? params.groupAllowFrom, storeAllowFrom, + dmPolicy: params.dmPolicy, }); const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined"; return { diff --git a/src/web/auto-reply/monitor/process-message.ts b/src/web/auto-reply/monitor/process-message.ts index 11a5cf8dee5..cf3b4d60554 100644 --- a/src/web/auto-reply/monitor/process-message.ts +++ b/src/web/auto-reply/monitor/process-message.ts @@ -28,6 +28,7 @@ import { getAgentScopedMediaLocalRoots } from "../../../media/local-roots.js"; import { readChannelAllowFromStore } from "../../../pairing/pairing-store.js"; import type { resolveAgentRoute } from "../../../routing/resolve-route.js"; import { jidToE164, normalizeE164 } from "../../../utils.js"; +import { resolveWhatsAppAccount } from "../../accounts.js"; import { newConnectionId } from "../../reconnect.js"; import { formatError } from "../../session.js"; import { deliverWebReply } from "../deliver-reply.js"; @@ -73,10 +74,11 @@ async function resolveWhatsAppCommandAuthorized(params: { return false; } - const configuredAllowFrom = params.cfg.channels?.whatsapp?.allowFrom ?? []; + const account = resolveWhatsAppAccount({ cfg: params.cfg, accountId: params.msg.accountId }); + const dmPolicy = account.dmPolicy ?? "pairing"; + const configuredAllowFrom = account.allowFrom ?? []; const configuredGroupAllowFrom = - params.cfg.channels?.whatsapp?.groupAllowFrom ?? - (configuredAllowFrom.length > 0 ? configuredAllowFrom : undefined); + account.groupAllowFrom ?? (configuredAllowFrom.length > 0 ? configuredAllowFrom : undefined); if (isGroup) { if (!configuredGroupAllowFrom || configuredGroupAllowFrom.length === 0) { @@ -88,11 +90,12 @@ async function resolveWhatsAppCommandAuthorized(params: { return normalizeAllowFromE164(configuredGroupAllowFrom).includes(senderE164); } - const storeAllowFrom = await readChannelAllowFromStore( - "whatsapp", - process.env, - params.msg.accountId, - ).catch(() => []); + const storeAllowFrom = + dmPolicy === "allowlist" + ? [] + : await readChannelAllowFromStore("whatsapp", process.env, params.msg.accountId).catch( + () => [], + ); const combinedAllowFrom = Array.from( new Set([...(configuredAllowFrom ?? []), ...storeAllowFrom]), ); diff --git a/src/web/inbound/access-control.test.ts b/src/web/inbound/access-control.test.ts index ac6c447ecdb..796488900f8 100644 --- a/src/web/inbound/access-control.test.ts +++ b/src/web/inbound/access-control.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import { + readAllowFromStoreMock, sendMessageMock, setAccessControlTestConfig, setupAccessControlTestHarness, @@ -108,4 +109,25 @@ describe("WhatsApp dmPolicy precedence", () => { const result = await checkUnauthorizedWorkDmSender(); expectSilentlyBlocked(result); }); + + it("does not merge persisted pairing approvals in allowlist mode", async () => { + setAccessControlTestConfig({ + channels: { + whatsapp: { + dmPolicy: "allowlist", + accounts: { + work: { + allowFrom: ["+15559999999"], + }, + }, + }, + }, + }); + readAllowFromStoreMock.mockResolvedValue(["+15550001111"]); + + const result = await checkUnauthorizedWorkDmSender(); + + expectSilentlyBlocked(result); + expect(readAllowFromStoreMock).not.toHaveBeenCalled(); + }); }); diff --git a/src/web/inbound/access-control.ts b/src/web/inbound/access-control.ts index 96671e7bc77..a7c2601e2b3 100644 --- a/src/web/inbound/access-control.ts +++ b/src/web/inbound/access-control.ts @@ -40,11 +40,10 @@ export async function checkInboundAccessControl(params: { }); const dmPolicy = account.dmPolicy ?? "pairing"; const configuredAllowFrom = account.allowFrom; - const storeAllowFrom = await readChannelAllowFromStore( - "whatsapp", - process.env, - account.accountId, - ).catch(() => []); + const storeAllowFrom = + dmPolicy === "allowlist" + ? [] + : await readChannelAllowFromStore("whatsapp", process.env, account.accountId).catch(() => []); // Without user config, default to self-only DM access so the owner can talk to themselves. const combinedAllowFrom = Array.from( new Set([...(configuredAllowFrom ?? []), ...storeAllowFrom]),