VYPR
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

1
  • cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*
    Range: <2026.3.31

Patches

1
d8c68c8d4265

fix: migrate Telegram pairing allowFrom to default account only (#58165)

https://github.com/openclaw/openclawpgondhi987Mar 31, 2026via nvd-ref
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

News mentions

0

No linked articles in our index yet.