From 6235720c8aee5eb7547f2d7028192cf004d4e66b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 29 May 2026 08:40:37 -0400 Subject: [PATCH] fix(slack): validate inbound timestamp parsing --- .../monitor/message-handler/prepare.test.ts | 12 ++++++++++++ .../src/monitor/message-handler/prepare.ts | 18 ++++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/extensions/slack/src/monitor/message-handler/prepare.test.ts b/extensions/slack/src/monitor/message-handler/prepare.test.ts index b0600a5ece2..c56ea897e5b 100644 --- a/extensions/slack/src/monitor/message-handler/prepare.test.ts +++ b/extensions/slack/src/monitor/message-handler/prepare.test.ts @@ -707,6 +707,18 @@ describe("slack prepareSlackMessage inbound contract", () => { expect(prepared?.ackReactionPromise).toBeNull(); }); + it("does not coerce malformed Slack timestamps into inbound event times", async () => { + const prepared = await prepareWithDefaultCtx( + createSlackMessage({ + ts: "0x10", + }), + ); + + assertPrepared(prepared); + expect(prepared.ctxPayload.Timestamp).toBeUndefined(); + expect(prepared.ctxPayload.MessageSid).toBe("0x10"); + }); + it("primes Slack status reactions when channel replies are message-tool-only", async () => { const slackCtx = createInboundSlackCtx({ cfg: { diff --git a/extensions/slack/src/monitor/message-handler/prepare.ts b/extensions/slack/src/monitor/message-handler/prepare.ts index 8c055d3b9f9..6f2debe1500 100644 --- a/extensions/slack/src/monitor/message-handler/prepare.ts +++ b/extensions/slack/src/monitor/message-handler/prepare.ts @@ -83,6 +83,7 @@ const SLACK_HISTORY_MEDIA_MAX_ATTACHMENTS = 4; const SLACK_HISTORY_MEDIA_MAX_BYTES = 10 * 1024 * 1024; const SLACK_HISTORY_MEDIA_IDLE_TIMEOUT_MS = 1_000; const SLACK_HISTORY_MEDIA_TOTAL_TIMEOUT_MS = 3_000; +const SLACK_TIMESTAMP_RE = /^\d+(?:\.\d+)?$/; function recordString( record: Record | undefined, @@ -104,6 +105,15 @@ function recordNullableString( return normalizeOptionalString(record[key]); } +function resolveSlackTimestampMs(ts: string | undefined): number | undefined { + const trimmed = ts?.trim(); + if (!trimmed || !SLACK_TIMESTAMP_RE.test(trimmed)) { + return undefined; + } + const parsed = Number(trimmed); + return Number.isFinite(parsed) ? Math.round(parsed * 1000) : undefined; +} + function mergeSlackAssistantThreadContext( primary: Omit | undefined, fallback: Omit | undefined, @@ -969,7 +979,7 @@ export async function prepareSlackMessage(params: { client: ctx.app.client, }) : null; - const timestamp = message.ts ? Math.round(Number(message.ts) * 1000) : undefined; + const timestamp = resolveSlackTimestampMs(message.ts); const senderName = pendingBody ? await resolveSenderName() : undefined; await recordDroppedChannelInboundHistory({ input: { @@ -1145,7 +1155,7 @@ export async function prepareSlackMessage(params: { const body = formatInboundEnvelope({ channel: "Slack", from: envelopeFrom, - timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined, + timestamp: resolveSlackTimestampMs(message.ts), body: textWithId, chatType, sender: { name: senderName, id: senderId }, @@ -1238,7 +1248,7 @@ export async function prepareSlackMessage(params: { channel: "slack", accountId: route.accountId, messageId: message.ts, - timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined, + timestamp: resolveSlackTimestampMs(message.ts), from: slackFrom, sender: { id: senderId, @@ -1335,7 +1345,7 @@ export async function prepareSlackMessage(params: { entry: { sender: senderName, body: rawBody, - timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined, + timestamp: resolveSlackTimestampMs(message.ts), messageId: message.ts, }, });