Medium severity6.5NVD Advisory· Published Apr 23, 2026· Updated Apr 28, 2026
CVE-2026-41340
CVE-2026-41340
Description
OpenClaw before 2026.3.31 contains an authentication boundary vulnerability where Telegram legacy allowFrom migration incorrectly fans default-account trust into all named accounts. Attackers can exploit this trust propagation to bypass authentication controls and gain unauthorized access to named accounts.
Affected products
1Patches
1d8c68c8d4265fix: migrate Telegram pairing allowFrom to default account only (#58165)
4 files changed · +56 −21
src/channels/read-only-account-inspect.telegram.ts+1 −1 modified@@ -59,7 +59,7 @@ export function listTelegramAccountIds(cfg: OpenClawConfig): string[] { }); } -function resolveDefaultTelegramAccountId(cfg: OpenClawConfig): string { +export function resolveDefaultTelegramAccountId(cfg: OpenClawConfig): string { const boundDefault = resolveDefaultAgentBoundAccountId(cfg, "telegram"); if (boundDefault) { return boundDefault;
src/commands/doctor-state-migrations.test.ts+41 −7 modified@@ -318,11 +318,12 @@ describe("doctor legacy state migrations", () => { }); }); - it("fans out legacy Telegram pairing allowFrom store to configured named accounts", async () => { + it("does not fan out legacy Telegram pairing allowFrom store to configured named accounts", async () => { const root = await makeTempRoot(); const cfg: OpenClawConfig = { channels: { telegram: { + defaultAccount: "bot2", accounts: { bot1: {}, bot2: {}, @@ -333,20 +334,53 @@ describe("doctor legacy state migrations", () => { const { oauthDir, detected, result } = await runTelegramAllowFromMigration({ root, cfg }); expect(detected.pairingAllowFrom.hasLegacyTelegram).toBe(true); expect( - detected.pairingAllowFrom.copyPlans.map((plan) => path.basename(plan.targetPath)).toSorted(), - ).toEqual(["telegram-bot1-allowFrom.json", "telegram-bot2-allowFrom.json"]); + detected.pairingAllowFrom.copyPlans.map((plan) => path.basename(plan.targetPath)), + ).toEqual(["telegram-bot2-allowFrom.json"]); expect(result.warnings).toEqual([]); const bot1Target = path.join(oauthDir, "telegram-bot1-allowFrom.json"); const bot2Target = path.join(oauthDir, "telegram-bot2-allowFrom.json"); - expect(fs.existsSync(bot1Target)).toBe(true); + const defaultTarget = path.join(oauthDir, "telegram-default-allowFrom.json"); + expect(fs.existsSync(bot1Target)).toBe(false); expect(fs.existsSync(bot2Target)).toBe(true); - expect(fs.existsSync(path.join(oauthDir, "telegram-default-allowFrom.json"))).toBe(false); - expect(JSON.parse(fs.readFileSync(bot1Target, "utf-8"))).toEqual({ + expect(fs.existsSync(defaultTarget)).toBe(false); + expect(JSON.parse(fs.readFileSync(bot2Target, "utf-8"))).toEqual({ version: 1, allowFrom: ["123456"], }); - expect(JSON.parse(fs.readFileSync(bot2Target, "utf-8"))).toEqual({ + }); + + it("migrates legacy Telegram pairing allowFrom store to the default agent bound account", async () => { + const root = await makeTempRoot(); + const cfg: OpenClawConfig = { + agents: { + list: [{ id: "ops", default: true }], + }, + bindings: [{ agentId: "ops", match: { channel: "telegram", accountId: "alerts" } }], + channels: { + telegram: { + accounts: { + alerts: {}, + backup: {}, + }, + }, + }, + }; + + const { oauthDir, detected, result } = await runTelegramAllowFromMigration({ root, cfg }); + expect(detected.pairingAllowFrom.hasLegacyTelegram).toBe(true); + expect( + detected.pairingAllowFrom.copyPlans.map((plan) => path.basename(plan.targetPath)), + ).toEqual(["telegram-alerts-allowFrom.json"]); + expect(result.warnings).toEqual([]); + + const alertsTarget = path.join(oauthDir, "telegram-alerts-allowFrom.json"); + const backupTarget = path.join(oauthDir, "telegram-backup-allowFrom.json"); + const defaultTarget = path.join(oauthDir, "telegram-default-allowFrom.json"); + expect(fs.existsSync(alertsTarget)).toBe(true); + expect(fs.existsSync(backupTarget)).toBe(false); + expect(fs.existsSync(defaultTarget)).toBe(false); + expect(JSON.parse(fs.readFileSync(alertsTarget, "utf-8"))).toEqual({ version: 1, allowFrom: ["123456"], });
src/infra/state-migrations.test.ts+6 −5 modified@@ -19,6 +19,7 @@ function createConfig(): OpenClawConfig { }, channels: { telegram: { + defaultAccount: "alpha", accounts: { beta: {}, alpha: {}, @@ -100,15 +101,13 @@ describe("state migrations", () => { expect(detected.pairingAllowFrom.hasLegacyTelegram).toBe(true); expect(detected.pairingAllowFrom.copyPlans.map((plan) => plan.targetPath)).toEqual([ resolveChannelAllowFromPath("telegram", env, "alpha"), - resolveChannelAllowFromPath("telegram", env, "beta"), ]); expect(detected.preview).toEqual([ `- Sessions: ${path.join(stateDir, "sessions")} → ${path.join(stateDir, "agents", "worker-1", "sessions")}`, `- Sessions: canonicalize legacy keys in ${path.join(stateDir, "agents", "worker-1", "sessions", "sessions.json")}`, `- Agent dir: ${path.join(stateDir, "agent")} → ${path.join(stateDir, "agents", "worker-1", "agent")}`, `- WhatsApp auth: ${path.join(stateDir, "credentials")} → ${path.join(stateDir, "credentials", "whatsapp", "default")} (keep oauth.json)`, `- Telegram pairing allowFrom: ${resolveChannelAllowFromPath("telegram", env)} → ${resolveChannelAllowFromPath("telegram", env, "alpha")}`, - `- Telegram pairing allowFrom: ${resolveChannelAllowFromPath("telegram", env)} → ${resolveChannelAllowFromPath("telegram", env, "beta")}`, ]); }); @@ -135,7 +134,6 @@ describe("state migrations", () => { "Moved WhatsApp auth creds.json → whatsapp/default", "Moved WhatsApp auth pre-key-1.json → whatsapp/default", `Copied Telegram pairing allowFrom → ${resolveChannelAllowFromPath("telegram", env, "alpha")}`, - `Copied Telegram pairing allowFrom → ${resolveChannelAllowFromPath("telegram", env, "beta")}`, ]); const mergedStore = JSON.parse( @@ -176,7 +174,10 @@ describe("state migrations", () => { fs.readFile(resolveChannelAllowFromPath("telegram", env, "alpha"), "utf8"), ).resolves.toBe('["123","456"]\n'); await expect( - fs.readFile(resolveChannelAllowFromPath("telegram", env, "beta"), "utf8"), - ).resolves.toBe('["123","456"]\n'); + fs.stat(resolveChannelAllowFromPath("telegram", env, "default")), + ).rejects.toMatchObject({ code: "ENOENT" }); + await expect( + fs.stat(resolveChannelAllowFromPath("telegram", env, "beta")), + ).rejects.toMatchObject({ code: "ENOENT" }); }); });
src/infra/state-migrations.ts+8 −8 modified@@ -2,7 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { resolveDefaultAgentId } from "../agents/agent-scope.js"; -import { listTelegramAccountIds } from "../channels/read-only-account-inspect.telegram.js"; +import { resolveDefaultTelegramAccountId } from "../channels/read-only-account-inspect.telegram.js"; import type { OpenClawConfig } from "../config/config.js"; import { resolveLegacyStateDirs, @@ -700,14 +700,14 @@ export async function detectLegacyStateMigrations(params: { fileExists(path.join(oauthDir, "creds.json")) && !fileExists(path.join(targetWhatsAppAuthDir, "creds.json")); const legacyTelegramAllowFromPath = resolveChannelAllowFromPath("telegram", env); + const targetTelegramAccountId = resolveDefaultTelegramAccountId(params.cfg); + const targetTelegramAllowFromPath = resolveChannelAllowFromPath( + "telegram", + env, + targetTelegramAccountId, + ); const telegramPairingAllowFromPlans = fileExists(legacyTelegramAllowFromPath) - ? Array.from( - new Set( - listTelegramAccountIds(params.cfg).map((accountId) => - resolveChannelAllowFromPath("telegram", env, accountId), - ), - ), - ) + ? [targetTelegramAllowFromPath] .filter((targetPath) => !fileExists(targetPath)) .map( (targetPath): FileCopyPlan => ({
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
3- github.com/openclaw/openclaw/commit/d8c68c8d4265ea6fa5e8c5e056534c351bddef37nvdPatch
- github.com/openclaw/openclaw/security/advisories/GHSA-f693-58pc-2gfrnvdVendor Advisory
- www.vulncheck.com/advisories/openclaw-authentication-boundary-bypass-via-telegram-legacy-allowfrom-migrationnvdThird Party Advisory
News mentions
0No linked articles in our index yet.