From 4e45a663e79c897b948de3f9f5c673d1f2cf39d5 Mon Sep 17 00:00:00 2001 From: HCL Date: Fri, 20 Mar 2026 13:13:01 +0800 Subject: [PATCH] fix(telegram): prevent silent wrong-bot routing when accountId not in config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a non-default accountId is specified but not found in the accounts config, resolveTelegramToken() falls through to channel-level defaults (botToken, tokenFile, env) — silently routing messages via the wrong bot's token. This is a cross-bot message leak with no error or warning. Root cause: extensions/telegram/src/token.ts:44-46, resolveAccountCfg() returns undefined for unknown accountIds but code continues to fallbacks. Introduced in e5bca0832f when Telegram moved to extensions/. Fix: return { token: "", source: "none" } with a diagnostic log when a non-default accountId is not found. Existing behavior for known accounts (with or without per-account tokens) preserved. Test: added "does not fall through when non-default accountId not in config" — 1/1 new, 10/10 existing unaffected. Closes #49383 Co-Authored-By: Claude Opus 4.6 Signed-off-by: HCL --- extensions/telegram/src/token.test.ts | 18 ++++++++++++++++++ extensions/telegram/src/token.ts | 11 +++++++++++ 2 files changed, 29 insertions(+) diff --git a/extensions/telegram/src/token.test.ts b/extensions/telegram/src/token.test.ts index c81e5d57b2c..74218f83ddd 100644 --- a/extensions/telegram/src/token.test.ts +++ b/extensions/telegram/src/token.test.ts @@ -188,6 +188,24 @@ describe("resolveTelegramToken", () => { expect(res.source).toBe("none"); }); + it("does not fall through to channel-level token when non-default accountId is not in config", () => { + vi.stubEnv("TELEGRAM_BOT_TOKEN", ""); + const cfg = { + channels: { + telegram: { + botToken: "wrong-bot-token", + accounts: { + knownBot: { botToken: "known-bot-token" }, + }, + }, + }, + } as OpenClawConfig; + + const res = resolveTelegramToken(cfg, { accountId: "unknownBot" }); + expect(res.token).toBe(""); + expect(res.source).toBe("none"); + }); + it("throws when botToken is an unresolved SecretRef object", () => { const cfg = { channels: { diff --git a/extensions/telegram/src/token.ts b/extensions/telegram/src/token.ts index 6727e9a7ee4..87ee2a7e11b 100644 --- a/extensions/telegram/src/token.ts +++ b/extensions/telegram/src/token.ts @@ -44,6 +44,17 @@ export function resolveTelegramToken( const accountCfg = resolveAccountCfg( accountId !== DEFAULT_ACCOUNT_ID ? accountId : DEFAULT_ACCOUNT_ID, ); + + // When a non-default accountId is explicitly specified but not found in config, + // return empty immediately — do NOT fall through to channel-level defaults, + // which would silently route the message via the wrong bot's token. + if (accountId !== DEFAULT_ACCOUNT_ID && !accountCfg) { + opts.logMissingFile?.( + `channels.telegram.accounts: unknown accountId "${accountId}" — not found in config, refusing channel-level fallback`, + ); + return { token: "", source: "none" }; + } + const accountTokenFile = accountCfg?.tokenFile?.trim(); if (accountTokenFile) { const token = tryReadSecretFileSync(