fix(slack): validate interaction payloads and handle malformed actions

This commit is contained in:
Peter Steinberger
2026-02-17 02:50:51 +01:00
parent bbb5fbc71f
commit d6226355e6
3 changed files with 66 additions and 5 deletions

View File

@@ -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();

View File

@@ -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;