mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 16:47:27 +00:00
fix(slack): validate interaction payloads and handle malformed actions
This commit is contained in:
@@ -201,6 +201,12 @@ For actions/directory reads, user token can be preferred when configured. For wr
|
||||
- Enable native Slack command handlers with `channels.slack.commands.native: true` (or global `commands.native: true`).
|
||||
- When native commands are enabled, register matching slash commands in Slack (`/<command>` names).
|
||||
- If native commands are not enabled, you can run a single configured slash command via `channels.slack.slashCommand`.
|
||||
- Native arg menus now adapt their rendering strategy:
|
||||
- up to 5 options: button blocks
|
||||
- 6-100 options: static select menu
|
||||
- more than 100 options: external select with async option filtering when interactivity options handlers are available
|
||||
- if encoded option values exceed Slack limits, the flow falls back to buttons
|
||||
- For long option payloads, Slash command argument menus use a confirm dialog before dispatching a selected value.
|
||||
|
||||
Default slash command settings:
|
||||
|
||||
@@ -286,6 +292,9 @@ Available action groups in current Slack tooling:
|
||||
- Member join/leave, channel created/renamed, and pin add/remove events are mapped into system events.
|
||||
- `channel_id_changed` can migrate channel config keys when `configWrites` is enabled.
|
||||
- Channel topic/purpose metadata is treated as untrusted context and can be injected into routing context.
|
||||
- Block actions and modal interactions emit structured `Slack interaction: ...` system events with rich payload fields:
|
||||
- block actions: selected values, labels, picker values, and `workflow_*` metadata
|
||||
- modal `view_submission` and `view_closed` events with routed channel metadata and form inputs
|
||||
|
||||
## Ack reactions
|
||||
|
||||
|
||||
@@ -228,6 +228,39 @@ describe("registerSlackInteractionEvents", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("ignores malformed action payloads after ack and logs warning", async () => {
|
||||
const { ctx, app, getHandler, runtimeLog } = createContext();
|
||||
registerSlackInteractionEvents({ ctx: ctx as never });
|
||||
const handler = getHandler();
|
||||
expect(handler).toBeTruthy();
|
||||
|
||||
const ack = vi.fn().mockResolvedValue(undefined);
|
||||
await handler!({
|
||||
ack,
|
||||
body: {
|
||||
user: { id: "U666" },
|
||||
channel: { id: "C1" },
|
||||
message: {
|
||||
ts: "777.888",
|
||||
text: "fallback",
|
||||
blocks: [
|
||||
{
|
||||
type: "actions",
|
||||
block_id: "verify_block",
|
||||
elements: [{ type: "button", action_id: "openclaw:verify" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
action: "not-an-action-object" as unknown as Record<string, unknown>,
|
||||
});
|
||||
|
||||
expect(ack).toHaveBeenCalled();
|
||||
expect(app.client.chat.update).not.toHaveBeenCalled();
|
||||
expect(enqueueSystemEventMock).not.toHaveBeenCalled();
|
||||
expect(runtimeLog).toHaveBeenCalledWith(expect.stringContaining("slack:interaction malformed"));
|
||||
});
|
||||
|
||||
it("escapes mrkdwn characters in confirmation labels", async () => {
|
||||
enqueueSystemEventMock.mockReset();
|
||||
const { ctx, app, getHandler } = createContext();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { SlackActionMiddlewareArgs } from "@slack/bolt";
|
||||
import type { Block, KnownBlock } from "@slack/web-api";
|
||||
import type { SlackMonitorContext } from "../context.js";
|
||||
import { enqueueSystemEvent } from "../../../infra/system-events.js";
|
||||
import { parseSlackModalPrivateMetadata } from "../../modal-metadata.js";
|
||||
import type { SlackMonitorContext } from "../context.js";
|
||||
|
||||
// Prefix for OpenClaw-generated action IDs to scope our handler
|
||||
const OPENCLAW_ACTION_PREFIX = "openclaw:";
|
||||
@@ -135,6 +135,13 @@ function summarizeRichTextPreview(value: unknown): string | undefined {
|
||||
return joined.length <= max ? joined : `${joined.slice(0, max - 1)}…`;
|
||||
}
|
||||
|
||||
function readInteractionAction(raw: unknown) {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
||||
return undefined;
|
||||
}
|
||||
return raw as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function summarizeAction(
|
||||
action: Record<string, unknown>,
|
||||
): Omit<InteractionSummary, "actionId" | "blockId"> {
|
||||
@@ -394,14 +401,26 @@ export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContex
|
||||
await ack();
|
||||
|
||||
// Extract action details using proper Bolt types
|
||||
const typedAction = action as unknown as Record<string, unknown> & {
|
||||
const typedAction = readInteractionAction(action);
|
||||
if (!typedAction) {
|
||||
ctx.runtime.log?.(
|
||||
`slack:interaction malformed action payload channel=${typedBody.channel?.id ?? typedBody.container?.channel_id ?? "unknown"} user=${
|
||||
typedBody.user?.id ?? "unknown"
|
||||
}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const typedActionWithText = typedAction as {
|
||||
action_id?: string;
|
||||
block_id?: string;
|
||||
type?: string;
|
||||
text?: { text?: string };
|
||||
};
|
||||
const actionId = typedAction.action_id ?? "unknown";
|
||||
const blockId = typedAction.block_id;
|
||||
const actionId =
|
||||
typeof typedActionWithText.action_id === "string"
|
||||
? typedActionWithText.action_id
|
||||
: "unknown";
|
||||
const blockId = typedActionWithText.block_id;
|
||||
const userId = typedBody.user?.id ?? "unknown";
|
||||
const channelId = typedBody.channel?.id ?? typedBody.container?.channel_id;
|
||||
const messageTs = typedBody.message?.ts ?? typedBody.container?.message_ts;
|
||||
@@ -454,7 +473,7 @@ export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContex
|
||||
const selectedLabel = formatInteractionSelectionLabel({
|
||||
actionId,
|
||||
summary: actionSummary,
|
||||
buttonText: typedAction.text?.text,
|
||||
buttonText: typedActionWithText.text?.text,
|
||||
});
|
||||
let updatedBlocks = originalBlocks.map((block) => {
|
||||
const typedBlock = block as InteractionMessageBlock;
|
||||
|
||||
Reference in New Issue
Block a user