VYPR
High severityNVD Advisory· Published Mar 19, 2026· Updated Mar 20, 2026

OpenClaw < 2026.2.25 - Authorization Bypass in Interactive Callbacks via Sender Check Skip

CVE-2026-32005

Description

OpenClaw versions prior to 2026.2.25 fail to enforce sender authorization checks for interactive callbacks including block_action, view_submission, and view_closed in shared workspace deployments. Unauthorized workspace members can bypass allowFrom restrictions and channel user allowlists to enqueue system-event text into active sessions.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.2.252026.2.25

Affected products

1

Patches

1
ce8c67c314b9

fix(slack): gate interactive system events by sender auth

https://github.com/openclaw/openclawPeter SteinbergerFeb 26, 2026via ghsa
6 files changed · +415 20
  • CHANGELOG.md+1 0 modified
    @@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai
     - Security/Signal: enforce DM/group authorization before reaction-only notification enqueue so unauthorized senders can no longer inject Signal reaction system events under `dmPolicy`/`groupPolicy`; reaction notifications now require channel access checks first. This ships in the next npm release (`2026.2.25`). Thanks @tdjackey for reporting.
     - Security/Discord + Slack reactions: enforce DM policy/allowlist authorization before reaction-event system enqueue in direct messages; Discord reaction handling now also honors DM/group-DM enablement and guild `groupPolicy` channel gating to keep reaction ingress aligned with normal message preflight. This ships in the next npm release (`2026.2.25`). Thanks @tdjackey for reporting.
     - Security/Telegram reactions: enforce `dmPolicy`/`allowFrom` and group allowlist authorization on `message_reaction` events before enqueueing reaction system events, preventing unauthorized reaction-triggered input in DMs and groups; ships in the next npm release (`2026.2.25`). Thanks @tdjackey for reporting.
    +- Security/Slack interactions: enforce channel/DM authorization and modal actor binding (`private_metadata.userId`) before enqueueing `block_action`/`view_submission`/`view_closed` system events, with regression coverage for unauthorized senders and missing/mismatched actor metadata. This ships in the next npm release (`2026.2.25`). Thanks @tdjackey for reporting.
     - Security/macOS beta onboarding: remove Anthropic OAuth sign-in and the legacy `oauth.json` onboarding path that exposed the PKCE verifier via OAuth `state`; this impacted the macOS beta onboarding path only. Anthropic subscription auth is now setup-token-only and will ship in the next npm release (`2026.2.25`). Thanks @zdi-disclosures for reporting.
     - Security/Nextcloud Talk: drop replayed signed webhook events with persistent per-account replay dedupe across restarts, and reject unexpected webhook backend origins when account base URL is configured. Thanks @aristorechina for reporting.
     - Security/Gateway: harden `agents.files` path handling to block out-of-workspace symlink targets for `agents.files.get`/`agents.files.set`, keep in-workspace symlink targets supported, and add gateway regression coverage for both blocked escapes and allowed in-workspace symlinks. Thanks @tdjackey for reporting.
    
  • src/slack/modal-metadata.test.ts+4 0 modified
    @@ -18,13 +18,15 @@ describe("parseSlackModalPrivateMetadata", () => {
               sessionKey: "agent:main:slack:channel:C1",
               channelId: "D123",
               channelType: "im",
    +          userId: "U123",
               ignored: "x",
             }),
           ),
         ).toEqual({
           sessionKey: "agent:main:slack:channel:C1",
           channelId: "D123",
           channelType: "im",
    +      userId: "U123",
         });
       });
     });
    @@ -37,11 +39,13 @@ describe("encodeSlackModalPrivateMetadata", () => {
               sessionKey: "agent:main:slack:channel:C1",
               channelId: "",
               channelType: "im",
    +          userId: "U123",
             }),
           ),
         ).toEqual({
           sessionKey: "agent:main:slack:channel:C1",
           channelType: "im",
    +      userId: "U123",
         });
       });
     
    
  • src/slack/modal-metadata.ts+3 0 modified
    @@ -2,6 +2,7 @@ export type SlackModalPrivateMetadata = {
       sessionKey?: string;
       channelId?: string;
       channelType?: string;
    +  userId?: string;
     };
     
     const SLACK_PRIVATE_METADATA_MAX = 3000;
    @@ -20,6 +21,7 @@ export function parseSlackModalPrivateMetadata(raw: unknown): SlackModalPrivateM
           sessionKey: normalizeString(parsed.sessionKey),
           channelId: normalizeString(parsed.channelId),
           channelType: normalizeString(parsed.channelType),
    +      userId: normalizeString(parsed.userId),
         };
       } catch {
         return {};
    @@ -31,6 +33,7 @@ export function encodeSlackModalPrivateMetadata(input: SlackModalPrivateMetadata
         ...(input.sessionKey ? { sessionKey: input.sessionKey } : {}),
         ...(input.channelId ? { channelId: input.channelId } : {}),
         ...(input.channelType ? { channelType: input.channelType } : {}),
    +    ...(input.userId ? { userId: input.userId } : {}),
       };
       const encoded = JSON.stringify(payload);
       if (encoded.length > SLACK_PRIVATE_METADATA_MAX) {
    
  • src/slack/monitor/auth.ts+142 2 modified
    @@ -1,6 +1,12 @@
     import { readChannelAllowFromStore } from "../../pairing/pairing-store.js";
    -import { allowListMatches, normalizeAllowList, normalizeAllowListLower } from "./allow-list.js";
    -import type { SlackMonitorContext } from "./context.js";
    +import {
    +  allowListMatches,
    +  normalizeAllowList,
    +  normalizeAllowListLower,
    +  resolveSlackUserAllowed,
    +} from "./allow-list.js";
    +import { resolveSlackChannelConfig } from "./channel-config.js";
    +import { normalizeSlackChannelType, type SlackMonitorContext } from "./context.js";
     
     export async function resolveSlackEffectiveAllowFrom(ctx: SlackMonitorContext) {
       const storeAllowFrom =
    @@ -27,3 +33,137 @@ export function isSlackSenderAllowListed(params: {
         })
       );
     }
    +
    +export type SlackSystemEventAuthResult = {
    +  allowed: boolean;
    +  reason?:
    +    | "missing-sender"
    +    | "sender-mismatch"
    +    | "channel-not-allowed"
    +    | "dm-disabled"
    +    | "sender-not-allowlisted"
    +    | "sender-not-channel-allowed";
    +  channelType?: "im" | "mpim" | "channel" | "group";
    +  channelName?: string;
    +};
    +
    +export async function authorizeSlackSystemEventSender(params: {
    +  ctx: SlackMonitorContext;
    +  senderId?: string;
    +  channelId?: string;
    +  channelType?: string | null;
    +  expectedSenderId?: string;
    +}): Promise<SlackSystemEventAuthResult> {
    +  const senderId = params.senderId?.trim();
    +  if (!senderId) {
    +    return { allowed: false, reason: "missing-sender" };
    +  }
    +
    +  const expectedSenderId = params.expectedSenderId?.trim();
    +  if (expectedSenderId && expectedSenderId !== senderId) {
    +    return { allowed: false, reason: "sender-mismatch" };
    +  }
    +
    +  const channelId = params.channelId?.trim();
    +  let channelType = normalizeSlackChannelType(params.channelType, channelId);
    +  let channelName: string | undefined;
    +  if (channelId) {
    +    const info: {
    +      name?: string;
    +      type?: "im" | "mpim" | "channel" | "group";
    +    } = await params.ctx.resolveChannelName(channelId).catch(() => ({}));
    +    channelName = info.name;
    +    channelType = normalizeSlackChannelType(params.channelType ?? info.type, channelId);
    +    if (
    +      !params.ctx.isChannelAllowed({
    +        channelId,
    +        channelName,
    +        channelType,
    +      })
    +    ) {
    +      return {
    +        allowed: false,
    +        reason: "channel-not-allowed",
    +        channelType,
    +        channelName,
    +      };
    +    }
    +  }
    +
    +  const senderInfo: { name?: string } = await params.ctx
    +    .resolveUserName(senderId)
    +    .catch(() => ({}));
    +  const senderName = senderInfo.name;
    +
    +  const resolveAllowFromLower = async () =>
    +    (await resolveSlackEffectiveAllowFrom(params.ctx)).allowFromLower;
    +
    +  if (channelType === "im") {
    +    if (!params.ctx.dmEnabled || params.ctx.dmPolicy === "disabled") {
    +      return { allowed: false, reason: "dm-disabled", channelType, channelName };
    +    }
    +    if (params.ctx.dmPolicy !== "open") {
    +      const allowFromLower = await resolveAllowFromLower();
    +      const senderAllowListed = isSlackSenderAllowListed({
    +        allowListLower: allowFromLower,
    +        senderId,
    +        senderName,
    +        allowNameMatching: params.ctx.allowNameMatching,
    +      });
    +      if (!senderAllowListed) {
    +        return {
    +          allowed: false,
    +          reason: "sender-not-allowlisted",
    +          channelType,
    +          channelName,
    +        };
    +      }
    +    }
    +  } else if (!channelId) {
    +    // No channel context. Apply allowFrom if configured so we fail closed
    +    // for privileged interactive events when owner allowlist is present.
    +    const allowFromLower = await resolveAllowFromLower();
    +    if (allowFromLower.length > 0) {
    +      const senderAllowListed = isSlackSenderAllowListed({
    +        allowListLower: allowFromLower,
    +        senderId,
    +        senderName,
    +        allowNameMatching: params.ctx.allowNameMatching,
    +      });
    +      if (!senderAllowListed) {
    +        return { allowed: false, reason: "sender-not-allowlisted" };
    +      }
    +    }
    +  } else {
    +    const channelConfig = resolveSlackChannelConfig({
    +      channelId,
    +      channelName,
    +      channels: params.ctx.channelsConfig,
    +      defaultRequireMention: params.ctx.defaultRequireMention,
    +    });
    +    const channelUsersAllowlistConfigured =
    +      Array.isArray(channelConfig?.users) && channelConfig.users.length > 0;
    +    if (channelUsersAllowlistConfigured) {
    +      const channelUserAllowed = resolveSlackUserAllowed({
    +        allowList: channelConfig?.users,
    +        userId: senderId,
    +        userName: senderName,
    +        allowNameMatching: params.ctx.allowNameMatching,
    +      });
    +      if (!channelUserAllowed) {
    +        return {
    +          allowed: false,
    +          reason: "sender-not-channel-allowed",
    +          channelType,
    +          channelName,
    +        };
    +      }
    +    }
    +  }
    +
    +  return {
    +    allowed: true,
    +    channelType,
    +    channelName,
    +  };
    +}
    
  • src/slack/monitor/events/interactions.test.ts+202 6 modified
    @@ -30,6 +30,7 @@ type RegisteredViewHandler = (args: {
         view?: {
           id?: string;
           callback_id?: string;
    +      private_metadata?: string;
           root_view_id?: string;
           previous_view_id?: string;
           external_id?: string;
    @@ -58,7 +59,23 @@ type RegisteredViewClosedHandler = (args: {
       };
     }) => Promise<void>;
     
    -function createContext() {
    +function createContext(overrides?: {
    +  dmEnabled?: boolean;
    +  dmPolicy?: "open" | "allowlist" | "pairing" | "disabled";
    +  allowFrom?: string[];
    +  allowNameMatching?: boolean;
    +  channelsConfig?: Record<string, { users?: string[] }>;
    +  isChannelAllowed?: (params: {
    +    channelId?: string;
    +    channelName?: string;
    +    channelType?: "im" | "mpim" | "channel" | "group";
    +  }) => boolean;
    +  resolveUserName?: (userId: string) => Promise<{ name?: string }>;
    +  resolveChannelName?: (channelId: string) => Promise<{
    +    name?: string;
    +    type?: "im" | "mpim" | "channel" | "group";
    +  }>;
    +}) {
       let handler: RegisteredHandler | null = null;
       let viewHandler: RegisteredViewHandler | null = null;
       let viewClosedHandler: RegisteredViewClosedHandler | null = null;
    @@ -80,16 +97,50 @@ function createContext() {
       };
       const runtimeLog = vi.fn();
       const resolveSessionKey = vi.fn().mockReturnValue("agent:ops:slack:channel:C1");
    +  const isChannelAllowed = vi
    +    .fn<
    +      (params: {
    +        channelId?: string;
    +        channelName?: string;
    +        channelType?: "im" | "mpim" | "channel" | "group";
    +      }) => boolean
    +    >()
    +    .mockImplementation((params) => overrides?.isChannelAllowed?.(params) ?? true);
    +  const resolveUserName = vi
    +    .fn<(userId: string) => Promise<{ name?: string }>>()
    +    .mockImplementation((userId) => overrides?.resolveUserName?.(userId) ?? Promise.resolve({}));
    +  const resolveChannelName = vi
    +    .fn<
    +      (channelId: string) => Promise<{
    +        name?: string;
    +        type?: "im" | "mpim" | "channel" | "group";
    +      }>
    +    >()
    +    .mockImplementation(
    +      (channelId) => overrides?.resolveChannelName?.(channelId) ?? Promise.resolve({}),
    +    );
       const ctx = {
         app,
         runtime: { log: runtimeLog },
    +    dmEnabled: overrides?.dmEnabled ?? true,
    +    dmPolicy: overrides?.dmPolicy ?? ("open" as const),
    +    allowFrom: overrides?.allowFrom ?? [],
    +    allowNameMatching: overrides?.allowNameMatching ?? false,
    +    channelsConfig: overrides?.channelsConfig ?? {},
    +    defaultRequireMention: true,
    +    isChannelAllowed,
    +    resolveUserName,
    +    resolveChannelName,
         resolveSlackSystemEventSessionKey: resolveSessionKey,
       };
       return {
         ctx,
         app,
         runtimeLog,
         resolveSessionKey,
    +    isChannelAllowed,
    +    resolveUserName,
    +    resolveChannelName,
         getHandler: () => handler,
         getViewHandler: () => viewHandler,
         getViewClosedHandler: () => viewClosedHandler,
    @@ -168,7 +219,7 @@ describe("registerSlackInteractionEvents", () => {
         });
         expect(resolveSessionKey).toHaveBeenCalledWith({
           channelId: "C1",
    -      channelType: undefined,
    +      channelType: "channel",
         });
         expect(app.client.chat.update).toHaveBeenCalledTimes(1);
       });
    @@ -228,6 +279,85 @@ describe("registerSlackInteractionEvents", () => {
         );
       });
     
    +  it("blocks block actions from users outside configured channel users allowlist", async () => {
    +    enqueueSystemEventMock.mockClear();
    +    const { ctx, app, getHandler } = createContext({
    +      channelsConfig: {
    +        C1: { users: ["U_ALLOWED"] },
    +      },
    +    });
    +    registerSlackInteractionEvents({ ctx: ctx as never });
    +    const handler = getHandler();
    +    expect(handler).toBeTruthy();
    +
    +    const ack = vi.fn().mockResolvedValue(undefined);
    +    const respond = vi.fn().mockResolvedValue(undefined);
    +    await handler!({
    +      ack,
    +      respond,
    +      body: {
    +        user: { id: "U_DENIED" },
    +        channel: { id: "C1" },
    +        message: {
    +          ts: "201.202",
    +          blocks: [{ type: "actions", block_id: "verify_block", elements: [] }],
    +        },
    +      },
    +      action: {
    +        type: "button",
    +        action_id: "openclaw:verify",
    +        block_id: "verify_block",
    +      },
    +    });
    +
    +    expect(ack).toHaveBeenCalled();
    +    expect(enqueueSystemEventMock).not.toHaveBeenCalled();
    +    expect(app.client.chat.update).not.toHaveBeenCalled();
    +    expect(respond).toHaveBeenCalledWith({
    +      text: "You are not authorized to use this control.",
    +      response_type: "ephemeral",
    +    });
    +  });
    +
    +  it("blocks DM block actions when sender is not in allowFrom", async () => {
    +    enqueueSystemEventMock.mockClear();
    +    const { ctx, app, getHandler } = createContext({
    +      dmPolicy: "allowlist",
    +      allowFrom: ["U_OWNER"],
    +    });
    +    registerSlackInteractionEvents({ ctx: ctx as never });
    +    const handler = getHandler();
    +    expect(handler).toBeTruthy();
    +
    +    const ack = vi.fn().mockResolvedValue(undefined);
    +    const respond = vi.fn().mockResolvedValue(undefined);
    +    await handler!({
    +      ack,
    +      respond,
    +      body: {
    +        user: { id: "U_ATTACKER" },
    +        channel: { id: "D222" },
    +        message: {
    +          ts: "301.302",
    +          blocks: [{ type: "actions", block_id: "verify_block", elements: [] }],
    +        },
    +      },
    +      action: {
    +        type: "button",
    +        action_id: "openclaw:verify",
    +        block_id: "verify_block",
    +      },
    +    });
    +
    +    expect(ack).toHaveBeenCalled();
    +    expect(enqueueSystemEventMock).not.toHaveBeenCalled();
    +    expect(app.client.chat.update).not.toHaveBeenCalled();
    +    expect(respond).toHaveBeenCalledWith({
    +      text: "You are not authorized to use this control.",
    +      response_type: "ephemeral",
    +    });
    +  });
    +
       it("ignores malformed action payloads after ack and logs warning", async () => {
         enqueueSystemEventMock.mockClear();
         const { ctx, app, getHandler, runtimeLog } = createContext();
    @@ -338,7 +468,7 @@ describe("registerSlackInteractionEvents", () => {
         expect(ack).toHaveBeenCalled();
         expect(resolveSessionKey).toHaveBeenCalledWith({
           channelId: "C222",
    -      channelType: undefined,
    +      channelType: "channel",
         });
         expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1);
         const [eventText] = enqueueSystemEventMock.mock.calls[0] as [string];
    @@ -697,7 +827,11 @@ describe("registerSlackInteractionEvents", () => {
               previous_view_id: "VPREV",
               external_id: "deploy-ext-1",
               hash: "view-hash-1",
    -          private_metadata: JSON.stringify({ channelId: "D123", channelType: "im" }),
    +          private_metadata: JSON.stringify({
    +            channelId: "D123",
    +            channelType: "im",
    +            userId: "U777",
    +          }),
               state: {
                 values: {
                   env_block: {
    @@ -771,6 +905,59 @@ describe("registerSlackInteractionEvents", () => {
         );
       });
     
    +  it("blocks modal events when private metadata userId does not match submitter", async () => {
    +    enqueueSystemEventMock.mockClear();
    +    const { ctx, getViewHandler } = createContext();
    +    registerSlackInteractionEvents({ ctx: ctx as never });
    +    const viewHandler = getViewHandler();
    +    expect(viewHandler).toBeTruthy();
    +
    +    const ack = vi.fn().mockResolvedValue(undefined);
    +    await viewHandler!({
    +      ack,
    +      body: {
    +        user: { id: "U222" },
    +        view: {
    +          callback_id: "openclaw:deploy_form",
    +          private_metadata: JSON.stringify({
    +            channelId: "D123",
    +            channelType: "im",
    +            userId: "U111",
    +          }),
    +        },
    +      },
    +    } as never);
    +
    +    expect(ack).toHaveBeenCalled();
    +    expect(enqueueSystemEventMock).not.toHaveBeenCalled();
    +  });
    +
    +  it("blocks modal events when private metadata is missing userId", async () => {
    +    enqueueSystemEventMock.mockClear();
    +    const { ctx, getViewHandler } = createContext();
    +    registerSlackInteractionEvents({ ctx: ctx as never });
    +    const viewHandler = getViewHandler();
    +    expect(viewHandler).toBeTruthy();
    +
    +    const ack = vi.fn().mockResolvedValue(undefined);
    +    await viewHandler!({
    +      ack,
    +      body: {
    +        user: { id: "U222" },
    +        view: {
    +          callback_id: "openclaw:deploy_form",
    +          private_metadata: JSON.stringify({
    +            channelId: "D123",
    +            channelType: "im",
    +          }),
    +        },
    +      },
    +    } as never);
    +
    +    expect(ack).toHaveBeenCalled();
    +    expect(enqueueSystemEventMock).not.toHaveBeenCalled();
    +  });
    +
       it("captures modal input labels and picker values across block types", async () => {
         enqueueSystemEventMock.mockClear();
         const { ctx, getViewHandler } = createContext();
    @@ -786,6 +973,7 @@ describe("registerSlackInteractionEvents", () => {
             view: {
               id: "V400",
               callback_id: "openclaw:routing_form",
    +          private_metadata: JSON.stringify({ userId: "U444" }),
               state: {
                 values: {
                   env_block: {
    @@ -1001,6 +1189,7 @@ describe("registerSlackInteractionEvents", () => {
             view: {
               id: "V555",
               callback_id: "openclaw:long_richtext",
    +          private_metadata: JSON.stringify({ userId: "U555" }),
               state: {
                 values: {
                   richtext_block: {
    @@ -1054,7 +1243,10 @@ describe("registerSlackInteractionEvents", () => {
               previous_view_id: "VPREV900",
               external_id: "deploy-ext-900",
               hash: "view-hash-900",
    -          private_metadata: JSON.stringify({ sessionKey: "agent:main:slack:channel:C99" }),
    +          private_metadata: JSON.stringify({
    +            sessionKey: "agent:main:slack:channel:C99",
    +            userId: "U900",
    +          }),
               state: {
                 values: {
                   env_block: {
    @@ -1101,7 +1293,10 @@ describe("registerSlackInteractionEvents", () => {
           viewId: "V900",
           userId: "U900",
           isCleared: true,
    -      privateMetadata: JSON.stringify({ sessionKey: "agent:main:slack:channel:C99" }),
    +      privateMetadata: JSON.stringify({
    +        sessionKey: "agent:main:slack:channel:C99",
    +        userId: "U900",
    +      }),
           rootViewId: "VROOT900",
           previousViewId: "VPREV900",
           externalId: "deploy-ext-900",
    @@ -1131,6 +1326,7 @@ describe("registerSlackInteractionEvents", () => {
             view: {
               id: "V901",
               callback_id: "openclaw:deploy_form",
    +          private_metadata: JSON.stringify({ userId: "U901" }),
             },
           },
         });
    
  • src/slack/monitor/events/interactions.ts+63 12 modified
    @@ -2,6 +2,7 @@ import type { SlackActionMiddlewareArgs } from "@slack/bolt";
     import type { Block, KnownBlock } from "@slack/web-api";
     import { enqueueSystemEvent } from "../../../infra/system-events.js";
     import { parseSlackModalPrivateMetadata } from "../../modal-metadata.js";
    +import { authorizeSlackSystemEventSender } from "../auth.js";
     import type { SlackMonitorContext } from "../context.js";
     import { escapeSlackMrkdwn } from "../mrkdwn.js";
     
    @@ -78,6 +79,7 @@ type SlackModalBody = {
     type SlackModalEventBase = {
       callbackId: string;
       userId: string;
    +  expectedUserId?: string;
       viewId?: string;
       sessionRouting: ReturnType<typeof resolveModalSessionRouting>;
       payload: {
    @@ -366,11 +368,15 @@ function summarizeViewState(values: unknown): ModalInputSummary[] {
     
     function resolveModalSessionRouting(params: {
       ctx: SlackMonitorContext;
    -  privateMetadata: unknown;
    +  metadata: ReturnType<typeof parseSlackModalPrivateMetadata>;
     }): { sessionKey: string; channelId?: string; channelType?: string } {
    -  const metadata = parseSlackModalPrivateMetadata(params.privateMetadata);
    +  const metadata = params.metadata;
       if (metadata.sessionKey) {
    -    return { sessionKey: metadata.sessionKey };
    +    return {
    +      sessionKey: metadata.sessionKey,
    +      channelId: metadata.channelId,
    +      channelType: metadata.channelType,
    +    };
       }
       if (metadata.channelId) {
         return {
    @@ -416,17 +422,19 @@ function resolveSlackModalEventBase(params: {
       ctx: SlackMonitorContext;
       body: SlackModalBody;
     }): SlackModalEventBase {
    +  const metadata = parseSlackModalPrivateMetadata(params.body.view?.private_metadata);
       const callbackId = params.body.view?.callback_id ?? "unknown";
       const userId = params.body.user?.id ?? "unknown";
       const viewId = params.body.view?.id;
       const inputs = summarizeViewState(params.body.view?.state?.values);
       const sessionRouting = resolveModalSessionRouting({
         ctx: params.ctx,
    -    privateMetadata: params.body.view?.private_metadata,
    +    metadata,
       });
       return {
         callbackId,
         userId,
    +    expectedUserId: metadata.userId,
         viewId,
         sessionRouting,
         payload: {
    @@ -449,16 +457,17 @@ function resolveSlackModalEventBase(params: {
       };
     }
     
    -function emitSlackModalLifecycleEvent(params: {
    +async function emitSlackModalLifecycleEvent(params: {
       ctx: SlackMonitorContext;
       body: SlackModalBody;
       interactionType: SlackModalInteractionKind;
       contextPrefix: "slack:interaction:view" | "slack:interaction:view-closed";
    -}): void {
    -  const { callbackId, userId, viewId, sessionRouting, payload } = resolveSlackModalEventBase({
    -    ctx: params.ctx,
    -    body: params.body,
    -  });
    +}): Promise<void> {
    +  const { callbackId, userId, expectedUserId, viewId, sessionRouting, payload } =
    +    resolveSlackModalEventBase({
    +      ctx: params.ctx,
    +      body: params.body,
    +    });
       const isViewClosed = params.interactionType === "view_closed";
       const isCleared = params.body.is_cleared === true;
       const eventPayload = isViewClosed
    @@ -482,6 +491,27 @@ function emitSlackModalLifecycleEvent(params: {
         );
       }
     
    +  if (!expectedUserId) {
    +    params.ctx.runtime.log?.(
    +      `slack:interaction drop modal callback=${callbackId} user=${userId} reason=missing-expected-user`,
    +    );
    +    return;
    +  }
    +
    +  const auth = await authorizeSlackSystemEventSender({
    +    ctx: params.ctx,
    +    senderId: userId,
    +    channelId: sessionRouting.channelId,
    +    channelType: sessionRouting.channelType,
    +    expectedSenderId: expectedUserId,
    +  });
    +  if (!auth.allowed) {
    +    params.ctx.runtime.log?.(
    +      `slack:interaction drop modal callback=${callbackId} user=${userId} reason=${auth.reason ?? "unauthorized"}`,
    +    );
    +    return;
    +  }
    +
       enqueueSystemEvent(`Slack interaction: ${JSON.stringify(eventPayload)}`, {
         sessionKey: sessionRouting.sessionKey,
         contextKey: [params.contextPrefix, callbackId, viewId, userId].filter(Boolean).join(":"),
    @@ -497,7 +527,7 @@ function registerModalLifecycleHandler(params: {
     }) {
       params.register(params.matcher, async ({ ack, body }: SlackModalEventHandlerArgs) => {
         await ack();
    -    emitSlackModalLifecycleEvent({
    +    await emitSlackModalLifecycleEvent({
           ctx: params.ctx,
           body: body as SlackModalBody,
           interactionType: params.interactionType,
    @@ -557,6 +587,27 @@ export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContex
           const channelId = typedBody.channel?.id ?? typedBody.container?.channel_id;
           const messageTs = typedBody.message?.ts ?? typedBody.container?.message_ts;
           const threadTs = typedBody.container?.thread_ts;
    +      const auth = await authorizeSlackSystemEventSender({
    +        ctx,
    +        senderId: userId,
    +        channelId,
    +      });
    +      if (!auth.allowed) {
    +        ctx.runtime.log?.(
    +          `slack:interaction drop action=${actionId} user=${userId} channel=${channelId ?? "unknown"} reason=${auth.reason ?? "unauthorized"}`,
    +        );
    +        if (respond) {
    +          try {
    +            await respond({
    +              text: "You are not authorized to use this control.",
    +              response_type: "ephemeral",
    +            });
    +          } catch {
    +            // Best-effort feedback only.
    +          }
    +        }
    +        return;
    +      }
           const actionSummary = summarizeAction(typedAction);
           const eventPayload: InteractionSummary = {
             interactionType: "block_action",
    @@ -581,7 +632,7 @@ export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContex
           // Pass undefined (not "unknown") to allow proper main session fallback
           const sessionKey = ctx.resolveSlackSystemEventSessionKey({
             channelId: channelId,
    -        channelType: undefined,
    +        channelType: auth.channelType,
           });
     
           // Build context key - only include defined values to avoid "unknown" noise
    

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

5

News mentions

0

No linked articles in our index yet.