diff --git a/packages/bots/slack/src/index.test.ts b/packages/bots/slack/src/index.test.ts index 44bdfaf8..8b17a427 100644 --- a/packages/bots/slack/src/index.test.ts +++ b/packages/bots/slack/src/index.test.ts @@ -33,6 +33,10 @@ function captureFetch(responses: Array<{ ok: boolean; ts?: string; error?: strin function signHeaders(body: string, signingKey = 'synthetic-signing-key'): Record { const timestamp = String(Math.floor(Date.now() / 1000)); + return signHeadersAt(body, timestamp, signingKey); +} + +function signHeadersAt(body: string, timestamp: string, signingKey = 'synthetic-signing-key'): Record { return { 'x-slack-request-timestamp': timestamp, 'x-slack-signature': slackSignature(body, timestamp, signingKey), @@ -199,6 +203,24 @@ describe('Slack bot adapter', () => { await closeable.close(); }); + it('uses the default timestamp tolerance when config contains an invalid value', async () => { + const { fetcher } = captureFetch(); + let server: Server | undefined; + const closeable = await bot.register( + ctx(), + [], + { port: 0, fetch: fetcher, timestampToleranceSeconds: Number.NaN, onServerReady: (value) => { server = value; } }, + ); + const body = JSON.stringify({ type: 'event_callback', event: { type: 'message', channel: 'C123' } }); + const staleTimestamp = String(Math.floor(Date.now() / 1000) - 600); + + const response = await post(serverPort(server!), '/slack/events', body, signHeadersAt(body, staleTimestamp)); + + expect(response.status).toBe(401); + expect(JSON.parse(response.body)).toEqual({ ok: false, error: 'invalid_signature' }); + await closeable.close(); + }); + it('dispatches signed block action payloads as interactions', async () => { const seen: BotEvent[] = []; const { calls, fetcher } = captureFetch([{ ok: true, ts: '1700000002.000100' }]); diff --git a/packages/bots/slack/src/index.ts b/packages/bots/slack/src/index.ts index 650fe337..babed8e3 100644 --- a/packages/bots/slack/src/index.ts +++ b/packages/bots/slack/src/index.ts @@ -416,12 +416,16 @@ function verifySlackSignature( signingKey: string, toleranceSeconds = DEFAULT_TIMESTAMP_TOLERANCE_SECONDS, ): boolean { + const effectiveToleranceSeconds = normalizeTimestampToleranceSeconds(toleranceSeconds); const timestamp = firstHeader(headers["x-slack-request-timestamp"]); const signature = firstHeader(headers["x-slack-signature"]); if (!timestamp || !signature) return false; const timestampNumber = Number(timestamp); if (!Number.isFinite(timestampNumber)) return false; - if (toleranceSeconds > 0 && Math.abs(Math.floor(Date.now() / 1000) - timestampNumber) > toleranceSeconds) { + if ( + effectiveToleranceSeconds > 0 + && Math.abs(Math.floor(Date.now() / 1000) - timestampNumber) > effectiveToleranceSeconds + ) { return false; } const expected = slackSignature(rawBody, timestamp, signingKey); @@ -430,6 +434,12 @@ function verifySlackSignature( return actual.length === expectedBuffer.length && timingSafeEqual(actual, expectedBuffer); } +function normalizeTimestampToleranceSeconds(value: number): number { + if (value === 0) return 0; + if (Number.isFinite(value) && value > 0) return value; + return DEFAULT_TIMESTAMP_TOLERANCE_SECONDS; +} + export function slackSignature(rawBody: string, timestamp: string, signingKey: string): string { const digest = createHmac("sha256", signingKey) .update(`v0:${timestamp}:${rawBody}`, "utf8")