VYPR
Medium severity5.3NVD Advisory· Published Apr 10, 2026· Updated Apr 13, 2026

CVE-2026-35654

CVE-2026-35654

Description

OpenClaw before 2026.3.25 contains an authorization bypass vulnerability in Microsoft Teams feedback invokes that allows unauthorized senders to record session feedback. Attackers can bypass sender allowlist checks via feedback invoke endpoints to trigger unauthorized feedback recording or reflection.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.3.282026.3.28

Affected products

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

Patches

1
c5415a474bb0

fix(msteams): align feedback invoke authorization (#55108)

https://github.com/openclaw/openclawJacob TomlinsonMar 26, 2026via ghsa
5 files changed · +617 90
  • extensions/msteams/src/monitor-handler/access.ts+126 0 added
    @@ -0,0 +1,126 @@
    +import {
    +  DEFAULT_ACCOUNT_ID,
    +  createChannelPairingController,
    +  evaluateSenderGroupAccessForPolicy,
    +  isDangerousNameMatchingEnabled,
    +  readStoreAllowFromForDmPolicy,
    +  resolveDefaultGroupPolicy,
    +  resolveDmGroupAccessWithLists,
    +  resolveEffectiveAllowFromLists,
    +  resolveSenderScopedGroupPolicy,
    +  type OpenClawConfig,
    +} from "../../runtime-api.js";
    +import { normalizeMSTeamsConversationId } from "../inbound.js";
    +import { resolveMSTeamsAllowlistMatch, resolveMSTeamsRouteConfig } from "../policy.js";
    +import { getMSTeamsRuntime } from "../runtime.js";
    +import type { MSTeamsTurnContext } from "../sdk-types.js";
    +
    +export type MSTeamsResolvedSenderAccess = Awaited<ReturnType<typeof resolveMSTeamsSenderAccess>>;
    +
    +export async function resolveMSTeamsSenderAccess(params: {
    +  cfg: OpenClawConfig;
    +  activity: MSTeamsTurnContext["activity"];
    +}) {
    +  const activity = params.activity;
    +  const msteamsCfg = params.cfg.channels?.msteams;
    +  const conversationId = normalizeMSTeamsConversationId(activity.conversation?.id ?? "unknown");
    +  const convType = activity.conversation?.conversationType?.toLowerCase();
    +  const isDirectMessage = convType === "personal" || (!convType && !activity.conversation?.isGroup);
    +  const senderId = activity.from?.aadObjectId ?? activity.from?.id ?? "unknown";
    +  const senderName = activity.from?.name ?? activity.from?.id ?? senderId;
    +
    +  const core = getMSTeamsRuntime();
    +  const pairing = createChannelPairingController({
    +    core,
    +    channel: "msteams",
    +    accountId: DEFAULT_ACCOUNT_ID,
    +  });
    +  const dmPolicy = msteamsCfg?.dmPolicy ?? "pairing";
    +  const storedAllowFrom = await readStoreAllowFromForDmPolicy({
    +    provider: "msteams",
    +    accountId: pairing.accountId,
    +    dmPolicy,
    +    readStore: pairing.readStoreForDmPolicy,
    +  });
    +  const configuredDmAllowFrom = (msteamsCfg?.allowFrom ?? []).map((entry) => String(entry));
    +  const groupAllowFrom = msteamsCfg?.groupAllowFrom;
    +  const resolvedAllowFromLists = resolveEffectiveAllowFromLists({
    +    allowFrom: configuredDmAllowFrom,
    +    groupAllowFrom,
    +    storeAllowFrom: storedAllowFrom,
    +    dmPolicy,
    +  });
    +  const defaultGroupPolicy = resolveDefaultGroupPolicy(params.cfg);
    +  const groupPolicy =
    +    !isDirectMessage && msteamsCfg
    +      ? (msteamsCfg.groupPolicy ?? defaultGroupPolicy ?? "allowlist")
    +      : "disabled";
    +  const effectiveGroupAllowFrom = resolvedAllowFromLists.effectiveGroupAllowFrom;
    +  const allowNameMatching = isDangerousNameMatchingEnabled(msteamsCfg);
    +  const channelGate = resolveMSTeamsRouteConfig({
    +    cfg: msteamsCfg,
    +    teamId: activity.channelData?.team?.id,
    +    teamName: activity.channelData?.team?.name,
    +    conversationId,
    +    channelName: activity.channelData?.channel?.name,
    +    allowNameMatching,
    +  });
    +
    +  // When a route-level (team/channel) allowlist is configured but the sender allowlist is
    +  // empty, resolveSenderScopedGroupPolicy would otherwise downgrade the policy to "open",
    +  // allowing any sender. To close this bypass (GHSA-g7cr-9h7q-4qxq), treat an empty sender
    +  // allowlist as deny-all whenever the route allowlist is active.
    +  const senderGroupPolicy =
    +    channelGate.allowlistConfigured && effectiveGroupAllowFrom.length === 0
    +      ? groupPolicy
    +      : resolveSenderScopedGroupPolicy({
    +          groupPolicy,
    +          groupAllowFrom: effectiveGroupAllowFrom,
    +        });
    +  const access = resolveDmGroupAccessWithLists({
    +    isGroup: !isDirectMessage,
    +    dmPolicy,
    +    groupPolicy: senderGroupPolicy,
    +    allowFrom: configuredDmAllowFrom,
    +    groupAllowFrom,
    +    storeAllowFrom: storedAllowFrom,
    +    groupAllowFromFallbackToAllowFrom: false,
    +    isSenderAllowed: (allowFrom) =>
    +      resolveMSTeamsAllowlistMatch({
    +        allowFrom,
    +        senderId,
    +        senderName,
    +        allowNameMatching,
    +      }).allowed,
    +  });
    +  const senderGroupAccess = evaluateSenderGroupAccessForPolicy({
    +    groupPolicy,
    +    groupAllowFrom: effectiveGroupAllowFrom,
    +    senderId,
    +    isSenderAllowed: (_senderId, allowFrom) =>
    +      resolveMSTeamsAllowlistMatch({
    +        allowFrom,
    +        senderId,
    +        senderName,
    +        allowNameMatching,
    +      }).allowed,
    +  });
    +
    +  return {
    +    msteamsCfg,
    +    pairing,
    +    isDirectMessage,
    +    conversationId,
    +    senderId,
    +    senderName,
    +    dmPolicy,
    +    channelGate,
    +    access,
    +    senderGroupAccess,
    +    configuredDmAllowFrom,
    +    effectiveDmAllowFrom: access.effectiveAllowFrom,
    +    effectiveGroupAllowFrom,
    +    allowNameMatching,
    +    groupPolicy,
    +  };
    +}
    
  • extensions/msteams/src/monitor-handler.feedback-authz.test.ts+370 0 added
    @@ -0,0 +1,370 @@
    +import { access, mkdtemp, readFile, rm } from "node:fs/promises";
    +import { tmpdir } from "node:os";
    +import path from "node:path";
    +import { beforeEach, describe, expect, it, vi } from "vitest";
    +import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "../runtime-api.js";
    +import type { MSTeamsConversationStore } from "./conversation-store.js";
    +import type { MSTeamsAdapter } from "./messenger.js";
    +import {
    +  type MSTeamsActivityHandler,
    +  type MSTeamsMessageHandlerDeps,
    +  registerMSTeamsHandlers,
    +} from "./monitor-handler.js";
    +import type { MSTeamsPollStore } from "./polls.js";
    +import { setMSTeamsRuntime } from "./runtime.js";
    +import type { MSTeamsTurnContext } from "./sdk-types.js";
    +
    +const feedbackReflectionMockState = vi.hoisted(() => ({
    +  runFeedbackReflection: vi.fn(),
    +}));
    +
    +vi.mock("./feedback-reflection.js", async () => {
    +  const actual = await vi.importActual<typeof import("./feedback-reflection.js")>(
    +    "./feedback-reflection.js",
    +  );
    +  return {
    +    ...actual,
    +    runFeedbackReflection: feedbackReflectionMockState.runFeedbackReflection,
    +  };
    +});
    +
    +function createRuntimeStub(readAllowFromStore: ReturnType<typeof vi.fn>): PluginRuntime {
    +  return {
    +    logging: {
    +      shouldLogVerbose: () => false,
    +    },
    +    channel: {
    +      debounce: {
    +        resolveInboundDebounceMs: () => 0,
    +        createInboundDebouncer: () => ({
    +          enqueue: async () => {},
    +        }),
    +      },
    +      pairing: {
    +        readAllowFromStore,
    +        upsertPairingRequest: vi.fn(async () => null),
    +      },
    +      routing: {
    +        resolveAgentRoute: ({ peer }: { peer: { kind: string; id: string } }) => ({
    +          sessionKey: `msteams:${peer.kind}:${peer.id}`,
    +          agentId: "default",
    +        }),
    +      },
    +      session: {
    +        resolveStorePath: (storePath?: string) => storePath ?? tmpdir(),
    +      },
    +    },
    +  } as unknown as PluginRuntime;
    +}
    +
    +function createActivityHandler(run = vi.fn(async () => undefined)): MSTeamsActivityHandler & {
    +  run: NonNullable<MSTeamsActivityHandler["run"]>;
    +} {
    +  let handler: MSTeamsActivityHandler & {
    +    run: NonNullable<MSTeamsActivityHandler["run"]>;
    +  };
    +  handler = {
    +    onMessage: () => handler,
    +    onMembersAdded: () => handler,
    +    onReactionsAdded: () => handler,
    +    onReactionsRemoved: () => handler,
    +    run,
    +  };
    +  return handler;
    +}
    +
    +function createDeps(params: {
    +  cfg: OpenClawConfig;
    +  readAllowFromStore?: ReturnType<typeof vi.fn>;
    +}): MSTeamsMessageHandlerDeps {
    +  const readAllowFromStore = params.readAllowFromStore ?? vi.fn(async () => []);
    +  setMSTeamsRuntime(createRuntimeStub(readAllowFromStore));
    +
    +  const adapter: MSTeamsAdapter = {
    +    continueConversation: async () => {},
    +    process: async () => {},
    +    updateActivity: async () => {},
    +    deleteActivity: async () => {},
    +  };
    +  const conversationStore: MSTeamsConversationStore = {
    +    upsert: async () => {},
    +    get: async () => null,
    +    list: async () => [],
    +    remove: async () => false,
    +    findByUserId: async () => null,
    +  };
    +  const pollStore: MSTeamsPollStore = {
    +    createPoll: async () => {},
    +    getPoll: async () => null,
    +    recordVote: async () => null,
    +  };
    +
    +  return {
    +    cfg: params.cfg,
    +    runtime: { error: vi.fn() } as unknown as RuntimeEnv,
    +    appId: "test-app-id",
    +    adapter,
    +    tokenProvider: {
    +      getAccessToken: async () => "token",
    +    },
    +    textLimit: 4000,
    +    mediaMaxBytes: 8 * 1024 * 1024,
    +    conversationStore,
    +    pollStore,
    +    log: {
    +      info: vi.fn(),
    +      error: vi.fn(),
    +      debug: vi.fn(),
    +    },
    +  };
    +}
    +
    +function createFeedbackInvokeContext(params: {
    +  reaction: "like" | "dislike";
    +  conversationId: string;
    +  conversationType: string;
    +  senderId: string;
    +  senderName?: string;
    +  teamId?: string;
    +  channelName?: string;
    +  comment?: string;
    +}): MSTeamsTurnContext {
    +  return {
    +    activity: {
    +      id: `invoke-${params.reaction}`,
    +      type: "invoke",
    +      name: "message/submitAction",
    +      channelId: "msteams",
    +      serviceUrl: "https://service.example.test",
    +      from: {
    +        id: `${params.senderId}-botframework`,
    +        aadObjectId: params.senderId,
    +        name: params.senderName ?? "Sender",
    +      },
    +      recipient: {
    +        id: "bot-id",
    +        name: "Bot",
    +      },
    +      conversation: {
    +        id: params.conversationId,
    +        conversationType: params.conversationType,
    +        tenantId: params.teamId ? "tenant-1" : undefined,
    +      },
    +      channelData: params.teamId
    +        ? {
    +            team: { id: params.teamId, name: "Team 1" },
    +            channel: params.channelName ? { name: params.channelName } : undefined,
    +          }
    +        : {},
    +      value: {
    +        actionName: "feedback",
    +        actionValue: {
    +          reaction: params.reaction,
    +          feedback: JSON.stringify({ feedbackText: params.comment ?? "feedback text" }),
    +        },
    +        replyToId: "bot-msg-1",
    +      },
    +    },
    +    sendActivity: vi.fn(async () => ({ id: "ignored" })),
    +    sendActivities: async () => [],
    +  } as unknown as MSTeamsTurnContext;
    +}
    +
    +async function expectFileMissing(filePath: string) {
    +  await expect(access(filePath)).rejects.toThrow();
    +}
    +
    +describe("msteams feedback invoke authz", () => {
    +  beforeEach(() => {
    +    feedbackReflectionMockState.runFeedbackReflection.mockReset();
    +    feedbackReflectionMockState.runFeedbackReflection.mockResolvedValue(undefined);
    +  });
    +
    +  it("records feedback for an allowlisted DM sender", async () => {
    +    const tmpDir = await mkdtemp(path.join(tmpdir(), "openclaw-msteams-feedback-"));
    +    try {
    +      const originalRun = vi.fn(async () => undefined);
    +      const handler = registerMSTeamsHandlers(
    +        createActivityHandler(originalRun),
    +        createDeps({
    +          cfg: {
    +            session: { store: tmpDir },
    +            channels: {
    +              msteams: {
    +                dmPolicy: "allowlist",
    +                allowFrom: ["owner-aad"],
    +              },
    +            },
    +          } as OpenClawConfig,
    +        }),
    +      ) as MSTeamsActivityHandler & {
    +        run: NonNullable<MSTeamsActivityHandler["run"]>;
    +      };
    +
    +      await handler.run(
    +        createFeedbackInvokeContext({
    +          reaction: "like",
    +          conversationId: "a:personal-chat;messageid=bot-msg-1",
    +          conversationType: "personal",
    +          senderId: "owner-aad",
    +          senderName: "Owner",
    +          comment: "allowed feedback",
    +        }),
    +      );
    +
    +      const transcript = await readFile(
    +        path.join(tmpDir, "msteams_direct_owner-aad.jsonl"),
    +        "utf-8",
    +      );
    +      expect(JSON.parse(transcript.trim())).toMatchObject({
    +        event: "feedback",
    +        messageId: "bot-msg-1",
    +        value: "positive",
    +        comment: "allowed feedback",
    +        sessionKey: "msteams:direct:owner-aad",
    +        conversationId: "a:personal-chat",
    +      });
    +      expect(originalRun).not.toHaveBeenCalled();
    +    } finally {
    +      await rm(tmpDir, { recursive: true, force: true });
    +    }
    +  });
    +
    +  it("keeps DM feedback allowed when team route allowlists exist", async () => {
    +    const tmpDir = await mkdtemp(path.join(tmpdir(), "openclaw-msteams-feedback-"));
    +    try {
    +      const originalRun = vi.fn(async () => undefined);
    +      const handler = registerMSTeamsHandlers(
    +        createActivityHandler(originalRun),
    +        createDeps({
    +          cfg: {
    +            session: { store: tmpDir },
    +            channels: {
    +              msteams: {
    +                dmPolicy: "allowlist",
    +                allowFrom: ["owner-aad"],
    +                teams: {
    +                  team123: {
    +                    channels: {
    +                      "19:group@thread.tacv2": { requireMention: false },
    +                    },
    +                  },
    +                },
    +              },
    +            },
    +          } as OpenClawConfig,
    +        }),
    +      ) as MSTeamsActivityHandler & {
    +        run: NonNullable<MSTeamsActivityHandler["run"]>;
    +      };
    +
    +      await handler.run(
    +        createFeedbackInvokeContext({
    +          reaction: "like",
    +          conversationId: "a:personal-chat;messageid=bot-msg-1",
    +          conversationType: "personal",
    +          senderId: "owner-aad",
    +          senderName: "Owner",
    +          comment: "allowed dm feedback",
    +        }),
    +      );
    +
    +      const transcript = await readFile(
    +        path.join(tmpDir, "msteams_direct_owner-aad.jsonl"),
    +        "utf-8",
    +      );
    +      expect(JSON.parse(transcript.trim())).toMatchObject({
    +        event: "feedback",
    +        value: "positive",
    +        comment: "allowed dm feedback",
    +        sessionKey: "msteams:direct:owner-aad",
    +      });
    +      expect(originalRun).not.toHaveBeenCalled();
    +    } finally {
    +      await rm(tmpDir, { recursive: true, force: true });
    +    }
    +  });
    +
    +  it("does not record feedback for a DM sender outside allowFrom", async () => {
    +    const tmpDir = await mkdtemp(path.join(tmpdir(), "openclaw-msteams-feedback-"));
    +    try {
    +      const originalRun = vi.fn(async () => undefined);
    +      const handler = registerMSTeamsHandlers(
    +        createActivityHandler(originalRun),
    +        createDeps({
    +          cfg: {
    +            session: { store: tmpDir },
    +            channels: {
    +              msteams: {
    +                dmPolicy: "allowlist",
    +                allowFrom: ["owner-aad"],
    +              },
    +            },
    +          } as OpenClawConfig,
    +        }),
    +      ) as MSTeamsActivityHandler & {
    +        run: NonNullable<MSTeamsActivityHandler["run"]>;
    +      };
    +
    +      await handler.run(
    +        createFeedbackInvokeContext({
    +          reaction: "like",
    +          conversationId: "a:personal-chat;messageid=bot-msg-1",
    +          conversationType: "personal",
    +          senderId: "attacker-aad",
    +          senderName: "Attacker",
    +          comment: "blocked feedback",
    +        }),
    +      );
    +
    +      await expectFileMissing(path.join(tmpDir, "msteams_direct_attacker-aad.jsonl"));
    +      expect(feedbackReflectionMockState.runFeedbackReflection).not.toHaveBeenCalled();
    +      expect(originalRun).not.toHaveBeenCalled();
    +    } finally {
    +      await rm(tmpDir, { recursive: true, force: true });
    +    }
    +  });
    +
    +  it("does not trigger reflection for a group sender outside groupAllowFrom", async () => {
    +    const tmpDir = await mkdtemp(path.join(tmpdir(), "openclaw-msteams-feedback-"));
    +    try {
    +      const originalRun = vi.fn(async () => undefined);
    +      const handler = registerMSTeamsHandlers(
    +        createActivityHandler(originalRun),
    +        createDeps({
    +          cfg: {
    +            session: { store: tmpDir },
    +            channels: {
    +              msteams: {
    +                groupPolicy: "allowlist",
    +                groupAllowFrom: ["owner-aad"],
    +                feedbackReflection: true,
    +              },
    +            },
    +          } as OpenClawConfig,
    +        }),
    +      ) as MSTeamsActivityHandler & {
    +        run: NonNullable<MSTeamsActivityHandler["run"]>;
    +      };
    +
    +      await handler.run(
    +        createFeedbackInvokeContext({
    +          reaction: "dislike",
    +          conversationId: "19:group@thread.tacv2;messageid=bot-msg-1",
    +          conversationType: "groupChat",
    +          senderId: "attacker-aad",
    +          senderName: "Attacker",
    +          teamId: "team-1",
    +          channelName: "General",
    +          comment: "blocked reflection",
    +        }),
    +      );
    +
    +      await expectFileMissing(path.join(tmpDir, "msteams_group_19_group_thread_tacv2.jsonl"));
    +      expect(feedbackReflectionMockState.runFeedbackReflection).not.toHaveBeenCalled();
    +      expect(originalRun).not.toHaveBeenCalled();
    +    } finally {
    +      await rm(tmpDir, { recursive: true, force: true });
    +    }
    +  });
    +});
    
  • extensions/msteams/src/monitor-handler/message-handler.authz.test.ts+46 2 modified
    @@ -7,6 +7,7 @@ import { createMSTeamsMessageHandler } from "./message-handler.js";
     describe("msteams monitor handler authz", () => {
       function createDeps(cfg: OpenClawConfig) {
         const readAllowFromStore = vi.fn(async () => ["attacker-aad"]);
    +    const upsertPairingRequest = vi.fn(async () => null);
         setMSTeamsRuntime({
           logging: { shouldLogVerbose: () => false },
           channel: {
    @@ -22,7 +23,7 @@ describe("msteams monitor handler authz", () => {
             },
             pairing: {
               readAllowFromStore,
    -          upsertPairingRequest: vi.fn(async () => null),
    +          upsertPairingRequest,
             },
             text: {
               hasControlCommand: () => false,
    @@ -56,7 +57,7 @@ describe("msteams monitor handler authz", () => {
           } as unknown as MSTeamsMessageHandlerDeps["log"],
         };
     
    -    return { conversationStore, deps, readAllowFromStore };
    +    return { conversationStore, deps, readAllowFromStore, upsertPairingRequest };
       }
     
       it("does not treat DM pairing-store entries as group allowlist entries", async () => {
    @@ -152,4 +153,47 @@ describe("msteams monitor handler authz", () => {
     
         expect(conversationStore.upsert).not.toHaveBeenCalled();
       });
    +
    +  it("keeps the DM pairing path wired through shared access resolution", async () => {
    +    const { deps, upsertPairingRequest } = createDeps({
    +      channels: {
    +        msteams: {
    +          dmPolicy: "pairing",
    +          allowFrom: [],
    +        },
    +      },
    +    } as OpenClawConfig);
    +
    +    const handler = createMSTeamsMessageHandler(deps);
    +    await handler({
    +      activity: {
    +        id: "msg-pairing",
    +        type: "message",
    +        text: "hello",
    +        from: {
    +          id: "new-user-id",
    +          aadObjectId: "new-user-aad",
    +          name: "New User",
    +        },
    +        recipient: {
    +          id: "bot-id",
    +          name: "Bot",
    +        },
    +        conversation: {
    +          id: "a:personal-chat",
    +          conversationType: "personal",
    +        },
    +        channelData: {},
    +        attachments: [],
    +      },
    +      sendActivity: vi.fn(async () => undefined),
    +    } as unknown as Parameters<typeof handler>[0]);
    +
    +    expect(upsertPairingRequest).toHaveBeenCalledWith({
    +      channel: "msteams",
    +      accountId: "default",
    +      id: "new-user-aad",
    +      meta: { name: "New User" },
    +    });
    +  });
     });
    
  • extensions/msteams/src/monitor-handler/message-handler.ts+24 87 modified
    @@ -1,23 +1,15 @@
     import {
    -  DEFAULT_ACCOUNT_ID,
       buildPendingHistoryContextFromMap,
       clearHistoryEntriesIfEnabled,
    -  createChannelPairingController,
       dispatchReplyFromConfigWithSettledDispatcher,
       DEFAULT_GROUP_HISTORY_LIMIT,
       logInboundDrop,
       evaluateSenderGroupAccessForPolicy,
    -  resolveSenderScopedGroupPolicy,
       recordPendingHistoryEntryIfEnabled,
       resolveDualTextControlCommandGate,
    -  resolveDefaultGroupPolicy,
    -  isDangerousNameMatchingEnabled,
    -  readStoreAllowFromForDmPolicy,
       resolveMentionGating,
       resolveInboundSessionEnvelopeContext,
       formatAllowlistMatchMeta,
    -  resolveEffectiveAllowFromLists,
    -  resolveDmGroupAccessWithLists,
       type HistoryEntry,
     } from "../../runtime-api.js";
     import {
    @@ -47,13 +39,13 @@ import {
       isMSTeamsGroupAllowed,
       resolveMSTeamsAllowlistMatch,
       resolveMSTeamsReplyPolicy,
    -  resolveMSTeamsRouteConfig,
     } from "../policy.js";
     import { extractMSTeamsPollVote } from "../polls.js";
     import { createMSTeamsReplyDispatcher } from "../reply-dispatcher.js";
     import { getMSTeamsRuntime } from "../runtime.js";
     import type { MSTeamsTurnContext } from "../sdk-types.js";
     import { recordMSTeamsSentMessage, wasMSTeamsMessageSent } from "../sent-message-cache.js";
    +import { resolveMSTeamsSenderAccess } from "./access.js";
     import { resolveMSTeamsInboundMedia } from "./inbound-media.js";
     
     export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
    @@ -70,11 +62,6 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
         log,
       } = deps;
       const core = getMSTeamsRuntime();
    -  const pairing = createChannelPairingController({
    -    core,
    -    channel: "msteams",
    -    accountId: DEFAULT_ACCOUNT_ID,
    -  });
       const logVerboseMessage = (message: string) => {
         if (core.logging.shouldLogVerbose()) {
           log.debug?.(message);
    @@ -142,77 +129,27 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
         const conversationId = normalizeMSTeamsConversationId(rawConversationId);
         const conversationMessageId = extractMSTeamsConversationMessageId(rawConversationId);
         const conversationType = conversation?.conversationType ?? "personal";
    -    const isGroupChat = conversationType === "groupChat" || conversation?.isGroup === true;
    -    const isChannel = conversationType === "channel";
    -    const isDirectMessage = !isGroupChat && !isChannel;
    -
    -    const senderName = from.name ?? from.id;
    -    const senderId = from.aadObjectId ?? from.id;
    -    const dmPolicy = msteamsCfg?.dmPolicy ?? "pairing";
    -    const storedAllowFrom = await readStoreAllowFromForDmPolicy({
    -      provider: "msteams",
    -      accountId: pairing.accountId,
    -      dmPolicy,
    -      readStore: pairing.readStoreForDmPolicy,
    -    });
    -    const useAccessGroups = cfg.commands?.useAccessGroups !== false;
    -
    -    // Check DM policy for direct messages.
    -    const dmAllowFrom = msteamsCfg?.allowFrom ?? [];
    -    const configuredDmAllowFrom = dmAllowFrom.map((v) => String(v));
    -    const groupAllowFrom = msteamsCfg?.groupAllowFrom;
    -    const resolvedAllowFromLists = resolveEffectiveAllowFromLists({
    -      allowFrom: configuredDmAllowFrom,
    -      groupAllowFrom,
    -      storeAllowFrom: storedAllowFrom,
    -      dmPolicy,
    -    });
    -
    -    const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
    -    const groupPolicy =
    -      !isDirectMessage && msteamsCfg
    -        ? (msteamsCfg.groupPolicy ?? defaultGroupPolicy ?? "allowlist")
    -        : "disabled";
    -    const effectiveGroupAllowFrom = resolvedAllowFromLists.effectiveGroupAllowFrom;
         const teamId = activity.channelData?.team?.id;
    -    const teamName = activity.channelData?.team?.name;
    -    const channelName = activity.channelData?.channel?.name;
    -    const channelGate = resolveMSTeamsRouteConfig({
    -      cfg: msteamsCfg,
    -      teamId,
    -      teamName,
    -      conversationId,
    -      channelName,
    -      allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg),
    -    });
    -    // When a route-level (team/channel) allowlist is configured but the sender allowlist is
    -    // empty, resolveSenderScopedGroupPolicy would otherwise downgrade the policy to "open",
    -    // allowing any sender. To close this bypass (GHSA-g7cr-9h7q-4qxq), treat an empty sender
    -    // allowlist as deny-all whenever the route allowlist is active.
    -    const senderGroupPolicy =
    -      channelGate.allowlistConfigured && effectiveGroupAllowFrom.length === 0
    -        ? groupPolicy
    -        : resolveSenderScopedGroupPolicy({
    -            groupPolicy,
    -            groupAllowFrom: effectiveGroupAllowFrom,
    -          });
    -    const access = resolveDmGroupAccessWithLists({
    -      isGroup: !isDirectMessage,
    +
    +    const {
           dmPolicy,
    -      groupPolicy: senderGroupPolicy,
    -      allowFrom: configuredDmAllowFrom,
    -      groupAllowFrom,
    -      storeAllowFrom: storedAllowFrom,
    -      groupAllowFromFallbackToAllowFrom: false,
    -      isSenderAllowed: (allowFrom) =>
    -        resolveMSTeamsAllowlistMatch({
    -          allowFrom,
    -          senderId,
    -          senderName,
    -          allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg),
    -        }).allowed,
    +      senderId,
    +      senderName,
    +      pairing,
    +      isDirectMessage,
    +      channelGate,
    +      access,
    +      configuredDmAllowFrom,
    +      effectiveDmAllowFrom,
    +      effectiveGroupAllowFrom,
    +      allowNameMatching,
    +      groupPolicy,
    +    } = await resolveMSTeamsSenderAccess({
    +      cfg,
    +      activity,
         });
    -    const effectiveDmAllowFrom = access.effectiveAllowFrom;
    +    const useAccessGroups = cfg.commands?.useAccessGroups !== false;
    +    const isChannel = conversationType === "channel";
     
         if (isDirectMessage && msteamsCfg && access.decision !== "allow") {
           if (access.reason === "dmPolicy=disabled") {
    @@ -223,7 +160,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
             allowFrom: effectiveDmAllowFrom,
             senderId,
             senderName,
    -        allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg),
    +        allowNameMatching,
           });
           if (access.decision === "pairing") {
             const request = await pairing.upsertPairingRequest({
    @@ -265,7 +202,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
                 allowFrom,
                 senderId,
                 senderName,
    -            allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg),
    +            allowNameMatching,
               }).allowed,
           });
     
    @@ -286,7 +223,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
               allowFrom: effectiveGroupAllowFrom,
               senderId,
               senderName,
    -          allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg),
    +          allowNameMatching,
             });
             log.debug?.("dropping group message (not in groupAllowFrom)", {
               sender: senderId,
    @@ -303,14 +240,14 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
           allowFrom: commandDmAllowFrom,
           senderId,
           senderName,
    -      allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg),
    +      allowNameMatching,
         });
         const groupAllowedForCommands = isMSTeamsGroupAllowed({
           groupPolicy: "allowlist",
           allowFrom: effectiveGroupAllowFrom,
           senderId,
           senderName,
    -      allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg),
    +      allowNameMatching,
         });
         const { commandAuthorized, shouldBlock } = resolveDualTextControlCommandGate({
           useAccessGroups,
    
  • extensions/msteams/src/monitor-handler.ts+51 1 modified
    @@ -1,9 +1,10 @@
    -import type { OpenClawConfig, RuntimeEnv } from "../runtime-api.js";
    +import { type OpenClawConfig, type RuntimeEnv } from "../runtime-api.js";
     import type { MSTeamsConversationStore } from "./conversation-store.js";
     import { buildFeedbackEvent, runFeedbackReflection } from "./feedback-reflection.js";
     import { buildFileInfoCard, parseFileConsentInvoke, uploadToConsentUrl } from "./file-consent.js";
     import { normalizeMSTeamsConversationId } from "./inbound.js";
     import type { MSTeamsAdapter } from "./messenger.js";
    +import { resolveMSTeamsSenderAccess } from "./monitor-handler/access.js";
     import { createMSTeamsMessageHandler } from "./monitor-handler/message-handler.js";
     import type { MSTeamsMonitorLogger } from "./monitor-types.js";
     import { getPendingUpload, removePendingUpload } from "./pending-uploads.js";
    @@ -46,6 +47,51 @@ export type MSTeamsMessageHandlerDeps = {
       log: MSTeamsMonitorLogger;
     };
     
    +async function isFeedbackInvokeAuthorized(
    +  context: MSTeamsTurnContext,
    +  deps: MSTeamsMessageHandlerDeps,
    +): Promise<boolean> {
    +  const resolved = await resolveMSTeamsSenderAccess({
    +    cfg: deps.cfg,
    +    activity: context.activity,
    +  });
    +  const { msteamsCfg, isDirectMessage, conversationId, senderId } = resolved;
    +  if (!msteamsCfg) {
    +    return true;
    +  }
    +
    +  if (isDirectMessage && resolved.access.decision !== "allow") {
    +    deps.log.debug?.("dropping feedback invoke (dm sender not allowlisted)", {
    +      sender: senderId,
    +      conversationId,
    +    });
    +    return false;
    +  }
    +
    +  if (
    +    !isDirectMessage &&
    +    resolved.channelGate.allowlistConfigured &&
    +    !resolved.channelGate.allowed
    +  ) {
    +    deps.log.debug?.("dropping feedback invoke (not in team/channel allowlist)", {
    +      conversationId,
    +      teamKey: resolved.channelGate.teamKey ?? "none",
    +      channelKey: resolved.channelGate.channelKey ?? "none",
    +    });
    +    return false;
    +  }
    +
    +  if (!isDirectMessage && !resolved.senderGroupAccess.allowed) {
    +    deps.log.debug?.("dropping feedback invoke (group sender not allowlisted)", {
    +      sender: senderId,
    +      conversationId,
    +    });
    +    return false;
    +  }
    +
    +  return true;
    +}
    +
     /**
      * Handle fileConsent/invoke activities for large file uploads.
      */
    @@ -178,6 +224,10 @@ async function handleFeedbackInvoke(
         return true; // Still consume the invoke
       }
     
    +  if (!(await isFeedbackInvokeAuthorized(context, deps))) {
    +    return true;
    +  }
    +
       // Extract user comment from the nested JSON string
       let userComment: string | undefined;
       if (value.actionValue?.feedback) {
    

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

4

News mentions

0

No linked articles in our index yet.