Skip to content

Commit 9b6904c

Browse files
committed
fix(slack): fail closed without webhook secret
1 parent 09447ea commit 9b6904c

3 files changed

Lines changed: 73 additions & 1 deletion

File tree

‎apps/sim/app/api/webhooks/slack/route.test.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,16 @@ describe('Slack app webhook route', () => {
9999
expect(mockHandleSlackChallenge).not.toHaveBeenCalled()
100100
})
101101

102+
it('treats a whitespace-only native signing secret as unconfigured', async () => {
103+
setEnv({ SLACK_SIGNING_SECRET: ' ' })
104+
105+
const response = await run(messageBody)
106+
107+
expect(response.status).toBe(500)
108+
expect(mockVerifySlackRequestSignature).not.toHaveBeenCalled()
109+
expect(mockFindWebhooksByRoutingKey).not.toHaveBeenCalled()
110+
})
111+
102112
it('verifies a signed request before answering the verification challenge', async () => {
103113
const body = { type: 'url_verification', challenge: 'challenge' }
104114
mockHandleSlackChallenge.mockReturnValue(new Response('challenge', { status: 200 }))

‎apps/sim/lib/webhooks/providers/slack.test.ts‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { createHmac } from 'node:crypto'
12
import { describe, expect, it } from 'vitest'
23
import {
34
handleSlackChallenge,
@@ -23,6 +24,66 @@ describe('slackHandler responses', () => {
2324
})
2425
})
2526

27+
describe('slackHandler request verification', () => {
28+
const rawBody = JSON.stringify({ type: 'event_callback' })
29+
30+
function signedRequest(signingSecret: string, timestamp: string, body = rawBody): Request {
31+
const signature = createHmac('sha256', signingSecret)
32+
.update(`v0:${timestamp}:${body}`, 'utf8')
33+
.digest('hex')
34+
return new Request('https://sim.test/api/webhooks/trigger/slack', {
35+
method: 'POST',
36+
headers: {
37+
'x-slack-request-timestamp': timestamp,
38+
'x-slack-signature': `v0=${signature}`,
39+
},
40+
})
41+
}
42+
43+
function verify(request: Request, providerConfig: Record<string, unknown>, body = rawBody) {
44+
return slackHandler.verifyAuth!({
45+
webhook: {},
46+
workflow: {},
47+
request: request as unknown as import('next/server').NextRequest,
48+
rawBody: body,
49+
requestId: 'slack-auth-test',
50+
providerConfig,
51+
})
52+
}
53+
54+
it('fails closed when a legacy Slack webhook has no signing secret', async () => {
55+
const response = await verify(new Request('https://sim.test'), {})
56+
57+
expect(response?.status).toBe(401)
58+
})
59+
60+
it('accepts a correctly signed current request', async () => {
61+
const signingSecret = 'test-signing-secret'
62+
const timestamp = String(Math.floor(Date.now() / 1000))
63+
64+
expect(verify(signedRequest(signingSecret, timestamp), { signingSecret })).toBeNull()
65+
})
66+
67+
it('rejects a signature computed for different raw bytes', async () => {
68+
const signingSecret = 'test-signing-secret'
69+
const timestamp = String(Math.floor(Date.now() / 1000))
70+
const request = signedRequest(signingSecret, timestamp)
71+
72+
const response = await verify(request, { signingSecret }, `${rawBody} `)
73+
74+
expect(response?.status).toBe(401)
75+
})
76+
77+
it("rejects an otherwise valid signature outside Slack's five-minute replay window", async () => {
78+
const signingSecret = 'test-signing-secret'
79+
const timestamp = String(Math.floor(Date.now() / 1000) - 301)
80+
81+
const response = await verify(signedRequest(signingSecret, timestamp), { signingSecret })
82+
83+
expect(response?.status).toBe(401)
84+
})
85+
})
86+
2687
describe('slackHandler formatInput - Events API', () => {
2788
it('maps an app_mention event', async () => {
2889
const { input } = await slackHandler.formatInput!(

‎apps/sim/lib/webhooks/providers/slack.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -840,7 +840,8 @@ export const slackHandler: WebhookProviderHandler = {
840840
verifyAuth({ request, rawBody, requestId, providerConfig }: AuthContext) {
841841
const signingSecret = providerConfig.signingSecret as string | undefined
842842
if (!signingSecret) {
843-
return null
843+
logger.warn(`[${requestId}] Slack webhook signing secret not configured`)
844+
return new NextResponse('Unauthorized - Missing Slack signing secret', { status: 401 })
844845
}
845846
return verifySlackRequestSignature(signingSecret, request, rawBody, requestId)
846847
},

0 commit comments

Comments
 (0)