VYPR
Medium severity5.4NVD Advisory· Published Apr 23, 2026· Updated May 1, 2026

CVE-2026-41358

CVE-2026-41358

Description

OpenClaw before 2026.4.2 fails to filter Slack thread context by sender allowlist, allowing non-allowlisted messages to enter agent context. Attackers can inject unauthorized thread messages through allowlisted user replies to bypass sender access controls and manipulate model context.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.4.22026.4.2

Affected products

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

Patches

1
ac5bc4fb37be

Slack: filter thread context by allowlist (#58380)

https://github.com/openclaw/openclawJacob TomlinsonApr 2, 2026via ghsa
7 files changed · +526 8
  • CHANGELOG.md+1 0 modified
    @@ -37,6 +37,7 @@ Docs: https://docs.openclaw.ai
     - Podman/launch: remove noisy container output from `scripts/run-openclaw-podman.sh` and align the Podman install guidance with the quieter startup flow. (#59368) Thanks @sallyom.
     - MS Teams/streaming: strip already-streamed text from fallback block delivery when replies exceed the 4000-character streaming limit so long responses stop duplicating content. (#59297) Thanks @bradgroux.
     - MS Teams/logging: format non-`Error` failures with the shared unknown-error helper so logs stop collapsing caught SDK or Axios objects into `[object Object]`. (#59321) Thanks @bradgroux.
    +- Slack/thread context: filter thread starter and history by the effective conversation allowlist without dropping valid open-room, DM, or group DM context. (#58380)
     
     ## 2026.4.1-beta.1
     
    
  • extensions/slack/src/monitor/media.ts+11 1 modified
    @@ -337,6 +337,7 @@ export async function resolveSlackAttachmentContent(params: {
     export type SlackThreadStarter = {
       text: string;
       userId?: string;
    +  botId?: string;
       ts?: string;
       files?: SlackFile[];
     };
    @@ -391,7 +392,15 @@ export async function resolveSlackThreadStarter(params: {
           ts: params.threadTs,
           limit: 1,
           inclusive: true,
    -    })) as { messages?: Array<{ text?: string; user?: string; ts?: string; files?: SlackFile[] }> };
    +    })) as {
    +      messages?: Array<{
    +        text?: string;
    +        user?: string;
    +        bot_id?: string;
    +        ts?: string;
    +        files?: SlackFile[];
    +      }>;
    +    };
         const message = response?.messages?.[0];
         const text = (message?.text ?? "").trim();
         if (!message || !text) {
    @@ -400,6 +409,7 @@ export async function resolveSlackThreadStarter(params: {
         const starter: SlackThreadStarter = {
           text,
           userId: message.user,
    +      botId: message.bot_id,
           ts: message.ts,
           files: message.files,
         };
    
  • extensions/slack/src/monitor/message-handler/prepare.test-helpers.ts+2 1 modified
    @@ -2,14 +2,15 @@ import type { App } from "@slack/bolt";
     import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
     import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
     import type { ResolvedSlackAccount } from "../../accounts.js";
    +import type { SlackChannelConfigEntries } from "../channel-config.js";
     import { createSlackMonitorContext } from "../context.js";
     
     export function createInboundSlackTestContext(params: {
       cfg: OpenClawConfig;
       appClient?: App["client"];
       defaultRequireMention?: boolean;
       replyToMode?: "off" | "all" | "first";
    -  channelsConfig?: Record<string, { systemPrompt: string }>;
    +  channelsConfig?: SlackChannelConfigEntries;
     }) {
       return createSlackMonitorContext({
         cfg: params.cfg,
    
  • extensions/slack/src/monitor/message-handler/prepare.thread-context-allowlist.test.ts+288 0 added
    @@ -0,0 +1,288 @@
    +import fs from "node:fs";
    +import os from "node:os";
    +import path from "node:path";
    +import type { App } from "@slack/bolt";
    +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
    +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
    +import type { SlackMessageEvent } from "../../types.js";
    +
    +type PrepareSlackMessage = typeof import("./prepare.js").prepareSlackMessage;
    +type CreateInboundSlackTestContext =
    +  typeof import("./prepare.test-helpers.js").createInboundSlackTestContext;
    +type CreateSlackTestAccount = typeof import("./prepare.test-helpers.js").createSlackTestAccount;
    +
    +let prepareSlackMessage: PrepareSlackMessage;
    +let createInboundSlackTestContext: CreateInboundSlackTestContext;
    +let createSlackTestAccount: CreateSlackTestAccount;
    +let fixtureRoot = "";
    +let caseId = 0;
    +
    +async function loadSlackPrepareModules() {
    +  const [{ prepareSlackMessage: loadedPrepareSlackMessage }, helpers] = await Promise.all([
    +    import("./prepare.js"),
    +    import("./prepare.test-helpers.js"),
    +  ]);
    +  prepareSlackMessage = loadedPrepareSlackMessage;
    +  createInboundSlackTestContext = helpers.createInboundSlackTestContext;
    +  createSlackTestAccount = helpers.createSlackTestAccount;
    +}
    +
    +function makeTmpStorePath() {
    +  if (!fixtureRoot) {
    +    throw new Error("fixtureRoot missing");
    +  }
    +  const dir = path.join(fixtureRoot, `case-${caseId++}`);
    +  fs.mkdirSync(dir);
    +  return path.join(dir, "sessions.json");
    +}
    +
    +describe("prepareSlackMessage thread context allowlists", () => {
    +  beforeAll(async () => {
    +    await loadSlackPrepareModules();
    +    fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-slack-room-thread-context-"));
    +  });
    +
    +  afterAll(() => {
    +    if (fixtureRoot) {
    +      fs.rmSync(fixtureRoot, { recursive: true, force: true });
    +      fixtureRoot = "";
    +    }
    +  });
    +
    +  it("uses room users allowlist for thread context filtering", async () => {
    +    const replies = vi
    +      .fn()
    +      .mockResolvedValueOnce({
    +        messages: [{ text: "starter from room user", user: "U1", ts: "100.000" }],
    +      })
    +      .mockResolvedValueOnce({
    +        messages: [
    +          { text: "starter from room user", user: "U1", ts: "100.000" },
    +          { text: "assistant reply", bot_id: "B1", ts: "100.500" },
    +          { text: "allowed follow-up", user: "U1", ts: "100.800" },
    +          { text: "current message", user: "U1", ts: "101.000" },
    +        ],
    +        response_metadata: { next_cursor: "" },
    +      });
    +    const storePath = makeTmpStorePath();
    +    const ctx = createInboundSlackTestContext({
    +      cfg: {
    +        session: { store: storePath },
    +        channels: { slack: { enabled: true, replyToMode: "all", groupPolicy: "open" } },
    +      } as OpenClawConfig,
    +      appClient: { conversations: { replies } } as unknown as App["client"],
    +      defaultRequireMention: false,
    +      replyToMode: "all",
    +      channelsConfig: {
    +        C123: {
    +          users: ["U1"],
    +          requireMention: false,
    +        },
    +      },
    +    });
    +    ctx.allowFrom = ["u-owner"];
    +    ctx.resolveUserName = async (id: string) => ({
    +      name: id === "U1" ? "Alice" : "Owner",
    +    });
    +    ctx.resolveChannelName = async () => ({ name: "general", type: "channel" });
    +
    +    const prepared = await prepareSlackMessage({
    +      ctx,
    +      account: createSlackTestAccount({
    +        replyToMode: "all",
    +        thread: { initialHistoryLimit: 20 },
    +      }),
    +      message: {
    +        channel: "C123",
    +        channel_type: "channel",
    +        user: "U1",
    +        text: "current message",
    +        ts: "101.000",
    +        thread_ts: "100.000",
    +      } as SlackMessageEvent,
    +      opts: { source: "message" },
    +    });
    +
    +    expect(prepared).toBeTruthy();
    +    expect(prepared!.ctxPayload.ThreadStarterBody).toBe("starter from room user");
    +    expect(prepared!.ctxPayload.ThreadHistoryBody).toContain("starter from room user");
    +    expect(prepared!.ctxPayload.ThreadHistoryBody).toContain("assistant reply");
    +    expect(prepared!.ctxPayload.ThreadHistoryBody).toContain("allowed follow-up");
    +    expect(prepared!.ctxPayload.ThreadHistoryBody).not.toContain("current message");
    +    expect(replies).toHaveBeenCalledTimes(2);
    +  });
    +
    +  it("does not apply the owner allowlist to open-room thread context", async () => {
    +    const replies = vi
    +      .fn()
    +      .mockResolvedValueOnce({
    +        messages: [{ text: "starter from open room", user: "U2", ts: "200.000" }],
    +      })
    +      .mockResolvedValueOnce({
    +        messages: [
    +          { text: "starter from open room", user: "U2", ts: "200.000" },
    +          { text: "assistant reply", bot_id: "B1", ts: "200.500" },
    +          { text: "open-room follow-up", user: "U2", ts: "200.800" },
    +          { text: "current message", user: "U2", ts: "201.000" },
    +        ],
    +        response_metadata: { next_cursor: "" },
    +      });
    +    const storePath = makeTmpStorePath();
    +    const ctx = createInboundSlackTestContext({
    +      cfg: {
    +        session: { store: storePath },
    +        channels: { slack: { enabled: true, replyToMode: "all", groupPolicy: "open" } },
    +      } as OpenClawConfig,
    +      appClient: { conversations: { replies } } as unknown as App["client"],
    +      defaultRequireMention: false,
    +      replyToMode: "all",
    +      channelsConfig: {
    +        C124: {
    +          requireMention: false,
    +        },
    +      },
    +    });
    +    ctx.allowFrom = ["u-owner"];
    +    ctx.resolveUserName = async (id: string) => ({
    +      name: id === "U2" ? "Bob" : "Owner",
    +    });
    +    ctx.resolveChannelName = async () => ({ name: "general", type: "channel" });
    +
    +    const prepared = await prepareSlackMessage({
    +      ctx,
    +      account: createSlackTestAccount({
    +        replyToMode: "all",
    +        thread: { initialHistoryLimit: 20 },
    +      }),
    +      message: {
    +        channel: "C124",
    +        channel_type: "channel",
    +        user: "U2",
    +        text: "current message",
    +        ts: "201.000",
    +        thread_ts: "200.000",
    +      } as SlackMessageEvent,
    +      opts: { source: "message" },
    +    });
    +
    +    expect(prepared).toBeTruthy();
    +    expect(prepared!.ctxPayload.ThreadStarterBody).toBe("starter from open room");
    +    expect(prepared!.ctxPayload.ThreadHistoryBody).toContain("starter from open room");
    +    expect(prepared!.ctxPayload.ThreadHistoryBody).toContain("assistant reply");
    +    expect(prepared!.ctxPayload.ThreadHistoryBody).toContain("open-room follow-up");
    +    expect(prepared!.ctxPayload.ThreadHistoryBody).not.toContain("current message");
    +    expect(replies).toHaveBeenCalledTimes(2);
    +  });
    +
    +  it("does not apply the owner allowlist to open DMs when dmPolicy is open", async () => {
    +    const replies = vi
    +      .fn()
    +      .mockResolvedValueOnce({
    +        messages: [{ text: "starter from open dm", user: "U3", ts: "300.000" }],
    +      })
    +      .mockResolvedValueOnce({
    +        messages: [
    +          { text: "starter from open dm", user: "U3", ts: "300.000" },
    +          { text: "assistant reply", bot_id: "B1", ts: "300.500" },
    +          { text: "dm follow-up", user: "U3", ts: "300.800" },
    +          { text: "current message", user: "U3", ts: "301.000" },
    +        ],
    +        response_metadata: { next_cursor: "" },
    +      });
    +    const storePath = makeTmpStorePath();
    +    const ctx = createInboundSlackTestContext({
    +      cfg: {
    +        session: { store: storePath },
    +        channels: { slack: { enabled: true, replyToMode: "all", groupPolicy: "open" } },
    +      } as OpenClawConfig,
    +      appClient: { conversations: { replies } } as unknown as App["client"],
    +      defaultRequireMention: false,
    +      replyToMode: "all",
    +    });
    +    ctx.allowFrom = ["u-owner"];
    +    ctx.resolveUserName = async (id: string) => ({
    +      name: id === "U3" ? "Dana" : "Owner",
    +    });
    +
    +    const prepared = await prepareSlackMessage({
    +      ctx,
    +      account: createSlackTestAccount({
    +        replyToMode: "all",
    +        thread: { initialHistoryLimit: 20 },
    +      }),
    +      message: {
    +        channel: "D300",
    +        channel_type: "im",
    +        user: "U3",
    +        text: "current message",
    +        ts: "301.000",
    +        thread_ts: "300.000",
    +      } as SlackMessageEvent,
    +      opts: { source: "message" },
    +    });
    +
    +    expect(prepared).toBeTruthy();
    +    expect(prepared!.ctxPayload.ThreadStarterBody).toBe("starter from open dm");
    +    expect(prepared!.ctxPayload.ThreadHistoryBody).toContain("starter from open dm");
    +    expect(prepared!.ctxPayload.ThreadHistoryBody).toContain("assistant reply");
    +    expect(prepared!.ctxPayload.ThreadHistoryBody).toContain("dm follow-up");
    +    expect(prepared!.ctxPayload.ThreadHistoryBody).not.toContain("current message");
    +    expect(replies).toHaveBeenCalledTimes(2);
    +  });
    +
    +  it("does not apply the owner allowlist to MPIM thread context", async () => {
    +    const replies = vi
    +      .fn()
    +      .mockResolvedValueOnce({
    +        messages: [{ text: "starter from mpim", user: "U4", ts: "400.000" }],
    +      })
    +      .mockResolvedValueOnce({
    +        messages: [
    +          { text: "starter from mpim", user: "U4", ts: "400.000" },
    +          { text: "assistant reply", bot_id: "B1", ts: "400.500" },
    +          { text: "mpim follow-up", user: "U4", ts: "400.800" },
    +          { text: "current message", user: "U4", ts: "401.000" },
    +        ],
    +        response_metadata: { next_cursor: "" },
    +      });
    +    const storePath = makeTmpStorePath();
    +    const ctx = createInboundSlackTestContext({
    +      cfg: {
    +        session: { store: storePath },
    +        channels: { slack: { enabled: true, replyToMode: "all", groupPolicy: "open" } },
    +      } as OpenClawConfig,
    +      appClient: { conversations: { replies } } as unknown as App["client"],
    +      defaultRequireMention: false,
    +      replyToMode: "all",
    +    });
    +    ctx.allowFrom = ["u-owner"];
    +    ctx.resolveUserName = async (id: string) => ({
    +      name: id === "U4" ? "Evan" : "Owner",
    +    });
    +
    +    const prepared = await prepareSlackMessage({
    +      ctx,
    +      account: createSlackTestAccount({
    +        replyToMode: "all",
    +        thread: { initialHistoryLimit: 20 },
    +      }),
    +      message: {
    +        channel: "G400",
    +        channel_type: "mpim",
    +        user: "U4",
    +        text: "current message",
    +        ts: "401.000",
    +        thread_ts: "400.000",
    +      } as SlackMessageEvent,
    +      opts: { source: "message" },
    +    });
    +
    +    expect(prepared).toBeTruthy();
    +    expect(prepared!.ctxPayload.ThreadStarterBody).toBe("starter from mpim");
    +    expect(prepared!.ctxPayload.ThreadHistoryBody).toContain("starter from mpim");
    +    expect(prepared!.ctxPayload.ThreadHistoryBody).toContain("assistant reply");
    +    expect(prepared!.ctxPayload.ThreadHistoryBody).toContain("mpim follow-up");
    +    expect(prepared!.ctxPayload.ThreadHistoryBody).not.toContain("current message");
    +    expect(replies).toHaveBeenCalledTimes(2);
    +  });
    +});
    
  • extensions/slack/src/monitor/message-handler/prepare-thread-context.test.ts+146 0 added
    @@ -0,0 +1,146 @@
    +import fs from "node:fs";
    +import os from "node:os";
    +import path from "node:path";
    +import type { App } from "@slack/bolt";
    +import { resolveEnvelopeFormatOptions } from "openclaw/plugin-sdk/channel-inbound";
    +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
    +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
    +import type { SlackMessageEvent } from "../../types.js";
    +import { resolveSlackThreadContextData } from "./prepare-thread-context.js";
    +import { createInboundSlackTestContext, createSlackTestAccount } from "./prepare.test-helpers.js";
    +
    +describe("resolveSlackThreadContextData", () => {
    +  let fixtureRoot = "";
    +  let caseId = 0;
    +
    +  function makeTmpStorePath() {
    +    if (!fixtureRoot) {
    +      throw new Error("fixtureRoot missing");
    +    }
    +    const dir = path.join(fixtureRoot, `case-${caseId++}`);
    +    fs.mkdirSync(dir);
    +    return { dir, storePath: path.join(dir, "sessions.json") };
    +  }
    +
    +  beforeAll(() => {
    +    fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-slack-thread-context-"));
    +  });
    +
    +  afterAll(() => {
    +    if (fixtureRoot) {
    +      fs.rmSync(fixtureRoot, { recursive: true, force: true });
    +      fixtureRoot = "";
    +    }
    +  });
    +
    +  function createThreadContext(params: { replies: unknown }) {
    +    return createInboundSlackTestContext({
    +      cfg: {
    +        channels: { slack: { enabled: true, replyToMode: "all", groupPolicy: "open" } },
    +      } as OpenClawConfig,
    +      appClient: { conversations: { replies: params.replies } } as App["client"],
    +      defaultRequireMention: false,
    +      replyToMode: "all",
    +    });
    +  }
    +
    +  function createThreadMessage(overrides: Partial<SlackMessageEvent> = {}): SlackMessageEvent {
    +    return {
    +      channel: "C123",
    +      channel_type: "channel",
    +      user: "U1",
    +      text: "current message",
    +      ts: "101.000",
    +      thread_ts: "100.000",
    +      ...overrides,
    +    } as SlackMessageEvent;
    +  }
    +
    +  it("omits non-allowlisted starter text and thread history messages", async () => {
    +    const { storePath } = makeTmpStorePath();
    +    const replies = vi.fn().mockResolvedValue({
    +      messages: [
    +        { text: "starter secret", user: "U2", ts: "100.000" },
    +        { text: "assistant reply", bot_id: "B1", ts: "100.500" },
    +        { text: "blocked follow-up", user: "U2", ts: "100.700" },
    +        { text: "allowed follow-up", user: "U1", ts: "100.800" },
    +        { text: "current message", user: "U1", ts: "101.000" },
    +      ],
    +      response_metadata: { next_cursor: "" },
    +    });
    +    const ctx = createThreadContext({ replies });
    +    ctx.resolveUserName = async (id: string) => ({
    +      name: id === "U1" ? "Alice" : "Mallory",
    +    });
    +
    +    const result = await resolveSlackThreadContextData({
    +      ctx,
    +      account: createSlackTestAccount({ thread: { initialHistoryLimit: 20 } }),
    +      message: createThreadMessage(),
    +      isThreadReply: true,
    +      threadTs: "100.000",
    +      threadStarter: {
    +        text: "starter secret",
    +        userId: "U2",
    +        ts: "100.000",
    +      },
    +      roomLabel: "#general",
    +      storePath,
    +      sessionKey: "thread-session",
    +      allowFromLower: ["u1"],
    +      allowNameMatching: false,
    +      envelopeOptions: resolveEnvelopeFormatOptions({} as OpenClawConfig),
    +      effectiveDirectMedia: null,
    +    });
    +
    +    expect(result.threadStarterBody).toBeUndefined();
    +    expect(result.threadLabel).toBe("Slack thread #general");
    +    expect(result.threadHistoryBody).toContain("assistant reply");
    +    expect(result.threadHistoryBody).toContain("allowed follow-up");
    +    expect(result.threadHistoryBody).not.toContain("starter secret");
    +    expect(result.threadHistoryBody).not.toContain("blocked follow-up");
    +    expect(result.threadHistoryBody).not.toContain("current message");
    +    expect(replies).toHaveBeenCalledTimes(1);
    +  });
    +
    +  it("keeps starter text and history when allowNameMatching authorizes the sender", async () => {
    +    const { storePath } = makeTmpStorePath();
    +    const replies = vi.fn().mockResolvedValue({
    +      messages: [
    +        { text: "starter from Alice", user: "U1", ts: "100.000" },
    +        { text: "blocked follow-up", user: "U2", ts: "100.700" },
    +        { text: "current message", user: "U1", ts: "101.000" },
    +      ],
    +      response_metadata: { next_cursor: "" },
    +    });
    +    const ctx = createThreadContext({ replies });
    +    ctx.resolveUserName = async (id: string) => ({
    +      name: id === "U1" ? "Alice" : "Mallory",
    +    });
    +
    +    const result = await resolveSlackThreadContextData({
    +      ctx,
    +      account: createSlackTestAccount({ thread: { initialHistoryLimit: 20 } }),
    +      message: createThreadMessage(),
    +      isThreadReply: true,
    +      threadTs: "100.000",
    +      threadStarter: {
    +        text: "starter from Alice",
    +        userId: "U1",
    +        ts: "100.000",
    +      },
    +      roomLabel: "#general",
    +      storePath,
    +      sessionKey: "thread-session",
    +      allowFromLower: ["alice"],
    +      allowNameMatching: true,
    +      envelopeOptions: resolveEnvelopeFormatOptions({} as OpenClawConfig),
    +      effectiveDirectMedia: null,
    +    });
    +
    +    expect(result.threadStarterBody).toBe("starter from Alice");
    +    expect(result.threadLabel).toContain("starter from Alice");
    +    expect(result.threadHistoryBody).toContain("starter from Alice");
    +    expect(result.threadHistoryBody).not.toContain("blocked follow-up");
    +  });
    +});
    
  • extensions/slack/src/monitor/message-handler/prepare-thread-context.ts+66 6 modified
    @@ -3,6 +3,7 @@ import { readSessionUpdatedAt } from "openclaw/plugin-sdk/config-runtime";
     import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
     import type { ResolvedSlackAccount } from "../../accounts.js";
     import type { SlackMessageEvent } from "../../types.js";
    +import { resolveSlackAllowListMatch } from "../allow-list.js";
     import type { SlackMonitorContext } from "../context.js";
     import {
       resolveSlackMedia,
    @@ -19,6 +20,27 @@ export type SlackThreadContextData = {
       threadStarterMedia: SlackMediaResult[] | null;
     };
     
    +function isSlackThreadContextSenderAllowed(params: {
    +  allowFromLower: string[];
    +  allowNameMatching: boolean;
    +  userId?: string;
    +  userName?: string;
    +  botId?: string;
    +}): boolean {
    +  if (params.allowFromLower.length === 0 || params.botId) {
    +    return true;
    +  }
    +  if (!params.userId) {
    +    return false;
    +  }
    +  return resolveSlackAllowListMatch({
    +    allowList: params.allowFromLower,
    +    id: params.userId,
    +    name: params.userName,
    +    allowNameMatching: params.allowNameMatching,
    +  }).allowed;
    +}
    +
     export async function resolveSlackThreadContextData(params: {
       ctx: SlackMonitorContext;
       account: ResolvedSlackAccount;
    @@ -29,6 +51,8 @@ export async function resolveSlackThreadContextData(params: {
       roomLabel: string;
       storePath: string;
       sessionKey: string;
    +  allowFromLower: string[];
    +  allowNameMatching: boolean;
       envelopeOptions: ReturnType<
         typeof import("openclaw/plugin-sdk/channel-inbound").resolveEnvelopeFormatOptions
       >;
    @@ -51,7 +75,21 @@ export async function resolveSlackThreadContextData(params: {
       }
     
       const starter = params.threadStarter;
    -  if (starter?.text) {
    +  const starterSenderName =
    +    params.allowNameMatching && starter?.userId
    +      ? (await params.ctx.resolveUserName(starter.userId))?.name
    +      : undefined;
    +  const starterAllowed =
    +    !starter ||
    +    isSlackThreadContextSenderAllowed({
    +      allowFromLower: params.allowFromLower,
    +      allowNameMatching: params.allowNameMatching,
    +      userId: starter.userId,
    +      userName: starterSenderName,
    +      botId: starter.botId,
    +    });
    +
    +  if (starter?.text && starterAllowed) {
         threadStarterBody = starter.text;
         const snippet = starter.text.replace(/\s+/g, " ").slice(0, 80);
         threadLabel = `Slack thread ${params.roomLabel}${snippet ? `: ${snippet}` : ""}`;
    @@ -69,6 +107,9 @@ export async function resolveSlackThreadContextData(params: {
       } else {
         threadLabel = `Slack thread ${params.roomLabel}`;
       }
    +  if (starter?.text && !starterAllowed) {
    +    logVerbose("slack: omitted non-allowlisted thread starter from context");
    +  }
     
       const threadInitialHistoryLimit = params.account.config?.thread?.initialHistoryLimit ?? 20;
       threadSessionPreviousTimestamp = readSessionUpdatedAt({
    @@ -101,8 +142,25 @@ export async function resolveSlackThreadContextData(params: {
             }),
           );
     
    +      const allowedThreadHistory = threadHistory.filter((historyMsg) => {
    +        const msgUser = historyMsg.userId ? userMap.get(historyMsg.userId) : null;
    +        return isSlackThreadContextSenderAllowed({
    +          allowFromLower: params.allowFromLower,
    +          allowNameMatching: params.allowNameMatching,
    +          userId: historyMsg.userId,
    +          userName: msgUser?.name,
    +          botId: historyMsg.botId,
    +        });
    +      });
    +      const omittedHistoryCount = threadHistory.length - allowedThreadHistory.length;
    +      if (omittedHistoryCount > 0) {
    +        logVerbose(
    +          `slack: omitted ${omittedHistoryCount} non-allowlisted thread message(s) from context`,
    +        );
    +      }
    +
           const historyParts: string[] = [];
    -      for (const historyMsg of threadHistory) {
    +      for (const historyMsg of allowedThreadHistory) {
             const msgUser = historyMsg.userId ? userMap.get(historyMsg.userId) : null;
             const msgSenderName =
               msgUser?.name ?? (historyMsg.botId ? `Bot (${historyMsg.botId})` : "Unknown");
    @@ -120,10 +178,12 @@ export async function resolveSlackThreadContextData(params: {
               }),
             );
           }
    -      threadHistoryBody = historyParts.join("\n\n");
    -      logVerbose(
    -        `slack: populated thread history with ${threadHistory.length} messages for new session`,
    -      );
    +      if (historyParts.length > 0) {
    +        threadHistoryBody = historyParts.join("\n\n");
    +        logVerbose(
    +          `slack: populated thread history with ${allowedThreadHistory.length} messages for new session`,
    +        );
    +      }
         }
       }
     
    
  • extensions/slack/src/monitor/message-handler/prepare.ts+12 0 modified
    @@ -37,6 +37,7 @@ import { hasSlackThreadParticipation } from "../../sent-thread-cache.js";
     import { resolveSlackThreadContext } from "../../threading.js";
     import type { SlackMessageEvent } from "../../types.js";
     import {
    +  normalizeAllowListLower,
       normalizeSlackAllowOwnerEntry,
       resolveSlackAllowListMatch,
       resolveSlackUserAllowed,
    @@ -436,6 +437,15 @@ export async function prepareSlackMessage(params: {
       }).allowed;
       const channelUsersAllowlistConfigured =
         isRoom && Array.isArray(channelConfig?.users) && channelConfig.users.length > 0;
    +  const threadContextAllowFromLower = isRoom
    +    ? channelUsersAllowlistConfigured
    +      ? normalizeAllowListLower(channelConfig?.users)
    +      : []
    +    : isDirectMessage
    +      ? ctx.dmPolicy === "open"
    +        ? []
    +        : allowFromLower
    +      : [];
       const channelCommandAuthorized =
         isRoom && channelUsersAllowlistConfigured
           ? resolveSlackUserAllowed({
    @@ -669,6 +679,8 @@ export async function prepareSlackMessage(params: {
         roomLabel,
         storePath,
         sessionKey,
    +    allowFromLower: threadContextAllowFromLower,
    +    allowNameMatching: ctx.allowNameMatching,
         envelopeOptions,
         effectiveDirectMedia,
       });
    

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.