From cf81cb942b4a71159567ed0580a03295bdb95a51 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 15 Jan 2026 10:25:27 +0000 Subject: [PATCH] fix: guard Slack cmdarg decode --- .../monitor/slash.command-arg-menus.test.ts | 23 +++++++++++++++++++ src/slack/monitor/slash.ts | 20 ++++++++++++---- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/slack/monitor/slash.command-arg-menus.test.ts b/src/slack/monitor/slash.command-arg-menus.test.ts index 8bc34ffa2..9f47cb1bf 100644 --- a/src/slack/monitor/slash.command-arg-menus.test.ts +++ b/src/slack/monitor/slash.command-arg-menus.test.ts @@ -197,4 +197,27 @@ describe("Slack native command argument menus", () => { }), ); }); + + it("treats malformed percent-encoding as an invalid button (no throw)", async () => { + const { actions, postEphemeral, ctx, account } = createHarness(); + registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never }); + + const handler = actions.get("clawdbot_cmdarg"); + if (!handler) throw new Error("Missing arg-menu action handler"); + + await handler({ + ack: vi.fn().mockResolvedValue(undefined), + action: { value: "cmdarg|%E0%A4%A|mode|on|U1" }, + body: { user: { id: "U1" }, channel: { id: "C1" } }, + }); + + expect(postEphemeral).toHaveBeenCalledWith( + expect.objectContaining({ + token: "bot-token", + channel: "C1", + user: "U1", + text: "Sorry, that button is no longer valid.", + }), + ); + }); }); diff --git a/src/slack/monitor/slash.ts b/src/slack/monitor/slash.ts index 1c811a4dc..03db28eb9 100644 --- a/src/slack/monitor/slash.ts +++ b/src/slack/monitor/slash.ts @@ -72,11 +72,23 @@ function parseSlackCommandArgValue(raw?: string | null): { if (parts.length !== 5 || parts[0] !== SLACK_COMMAND_ARG_VALUE_PREFIX) return null; const [, command, arg, value, userId] = parts; if (!command || !arg || !value || !userId) return null; + const decode = (text: string) => { + try { + return decodeURIComponent(text); + } catch { + return null; + } + }; + const decodedCommand = decode(command); + const decodedArg = decode(arg); + const decodedValue = decode(value); + const decodedUserId = decode(userId); + if (!decodedCommand || !decodedArg || !decodedValue || !decodedUserId) return null; return { - command: decodeURIComponent(command), - arg: decodeURIComponent(arg), - value: decodeURIComponent(value), - userId: decodeURIComponent(userId), + command: decodedCommand, + arg: decodedArg, + value: decodedValue, + userId: decodedUserId, }; }