Skip to content

Commit ed326c8

Browse files
committed
feat(slack): add subtype field and signature verification to Slack trigger
1 parent 0f602f7 commit ed326c8

File tree

3 files changed

+105
-1
lines changed

3 files changed

+105
-1
lines changed

apps/sim/blocks/blocks/slack.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1634,8 +1634,21 @@ Do not include any explanations, markdown formatting, or other text outside the
16341634

16351635
// Trigger outputs (when used as webhook trigger)
16361636
event_type: { type: 'string', description: 'Type of Slack event that triggered the workflow' },
1637+
subtype: {
1638+
type: 'string',
1639+
description:
1640+
'Message subtype (e.g., channel_join, channel_leave, bot_message). Null for regular user messages',
1641+
},
16371642
channel_name: { type: 'string', description: 'Human-readable channel name' },
1643+
channel_type: {
1644+
type: 'string',
1645+
description: 'Type of channel (e.g., channel, group, im, mpim)',
1646+
},
16381647
user_name: { type: 'string', description: 'Username who triggered the event' },
1648+
bot_id: {
1649+
type: 'string',
1650+
description: 'Bot ID if the message was sent by a bot. Null for human users',
1651+
},
16391652
timestamp: { type: 'string', description: 'Message timestamp from the triggering event' },
16401653
thread_ts: {
16411654
type: 'string',

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

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import crypto from 'crypto'
12
import { createLogger } from '@sim/logger'
23
import { NextResponse } from 'next/server'
4+
import { safeCompare } from '@/lib/core/security/encryption'
35
import {
46
secureFetchWithPinnedIP,
57
validateUrlWithDNS,
68
} from '@/lib/core/security/input-validation.server'
79
import type {
10+
AuthContext,
811
FormatInputContext,
912
FormatInputResult,
1013
WebhookProviderHandler,
@@ -177,6 +180,44 @@ async function fetchSlackMessageText(
177180
}
178181
}
179182

183+
/** Maximum allowed timestamp skew (5 minutes) per Slack docs. */
184+
const SLACK_TIMESTAMP_MAX_SKEW = 300
185+
186+
/**
187+
* Validate Slack request signature using HMAC-SHA256.
188+
* Basestring format: `v0:{timestamp}:{rawBody}`
189+
* Signature header format: `v0={hex}`
190+
*/
191+
function validateSlackSignature(
192+
signingSecret: string,
193+
signature: string,
194+
timestamp: string,
195+
rawBody: string
196+
): boolean {
197+
try {
198+
if (!signingSecret || !signature || !rawBody) {
199+
return false
200+
}
201+
202+
if (!signature.startsWith('v0=')) {
203+
logger.warn('Slack signature has invalid format (missing v0= prefix)')
204+
return false
205+
}
206+
207+
const providedSignature = signature.substring(3)
208+
const basestring = `v0:${timestamp}:${rawBody}`
209+
const computedHash = crypto
210+
.createHmac('sha256', signingSecret)
211+
.update(basestring, 'utf8')
212+
.digest('hex')
213+
214+
return safeCompare(computedHash, providedSignature)
215+
} catch (error) {
216+
logger.error('Error validating Slack signature:', error)
217+
return false
218+
}
219+
}
220+
180221
/**
181222
* Handle Slack verification challenges
182223
*/
@@ -190,6 +231,39 @@ export function handleSlackChallenge(body: unknown): NextResponse | null {
190231
}
191232

192233
export const slackHandler: WebhookProviderHandler = {
234+
verifyAuth({ request, rawBody, requestId, providerConfig }: AuthContext) {
235+
const signingSecret = providerConfig.signingSecret as string | undefined
236+
if (!signingSecret) {
237+
return null
238+
}
239+
240+
const signature = request.headers.get('x-slack-signature')
241+
const timestamp = request.headers.get('x-slack-request-timestamp')
242+
243+
if (!signature || !timestamp) {
244+
logger.warn(`[${requestId}] Slack webhook missing signature or timestamp header`)
245+
return new NextResponse('Unauthorized - Missing Slack signature', { status: 401 })
246+
}
247+
248+
const now = Math.floor(Date.now() / 1000)
249+
const skew = Math.abs(now - Number(timestamp))
250+
if (skew > SLACK_TIMESTAMP_MAX_SKEW) {
251+
logger.warn(`[${requestId}] Slack webhook timestamp too old`, {
252+
timestamp,
253+
now,
254+
skew,
255+
})
256+
return new NextResponse('Unauthorized - Request timestamp too old', { status: 401 })
257+
}
258+
259+
if (!validateSlackSignature(signingSecret, signature, timestamp, rawBody)) {
260+
logger.warn(`[${requestId}] Slack signature verification failed`)
261+
return new NextResponse('Unauthorized - Invalid Slack signature', { status: 401 })
262+
}
263+
264+
return null
265+
},
266+
193267
handleChallenge(body: unknown) {
194268
return handleSlackChallenge(body)
195269
},
@@ -262,10 +336,13 @@ export const slackHandler: WebhookProviderHandler = {
262336
input: {
263337
event: {
264338
event_type: eventType,
339+
subtype: (rawEvent?.subtype as string) || null,
265340
channel,
266341
channel_name: '',
342+
channel_type: (rawEvent?.channel_type as string) || null,
267343
user: (rawEvent?.user as string) || '',
268344
user_name: '',
345+
bot_id: (rawEvent?.bot_id as string) || null,
269346
text,
270347
timestamp: messageTs,
271348
thread_ts: (rawEvent?.thread_ts as string) || '',

apps/sim/triggers/slack/webhook.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export const slackWebhookTrigger: TriggerConfig = {
6868
'If you don\'t have an app:<br><ul class="mt-1 ml-5 list-disc"><li>Create an app from scratch</li><li>Give it a name and select your workspace</li></ul>',
6969
'Go to "Basic Information", find the "Signing Secret", and paste it in the field above.',
7070
'Go to "OAuth & Permissions" and add bot token scopes:<br><ul class="mt-1 ml-5 list-disc"><li><code>app_mentions:read</code> - For viewing messages that tag your bot with an @</li><li><code>chat:write</code> - To send messages to channels your bot is a part of</li><li><code>files:read</code> - To access files and images shared in messages</li><li><code>reactions:read</code> - For listening to emoji reactions and fetching reacted-to message text</li></ul>',
71-
'Go to "Event Subscriptions":<br><ul class="mt-1 ml-5 list-disc"><li>Enable events</li><li>Under "Subscribe to Bot Events", add <code>app_mention</code> to listen to messages that mention your bot</li><li>For reaction events, also add <code>reaction_added</code> and/or <code>reaction_removed</code></li><li>Paste the Webhook URL above into the "Request URL" field</li></ul>',
71+
'Go to "Event Subscriptions":<br><ul class="mt-1 ml-5 list-disc"><li>Enable events</li><li>Under "Subscribe to Bot Events", add <code>app_mention</code> to listen to messages that mention your bot</li><li>To receive all channel messages, add <code>message.channels</code>. For DMs add <code>message.im</code>, for group DMs add <code>message.mpim</code>, for private channels add <code>message.groups</code></li><li>For reaction events, also add <code>reaction_added</code> and/or <code>reaction_removed</code></li><li>Paste the Webhook URL above into the "Request URL" field</li></ul>',
7272
'Go to "Install App" in the left sidebar and install the app into your desired Slack workspace and channel.',
7373
'Copy the "Bot User OAuth Token" (starts with <code>xoxb-</code>) and paste it in the Bot Token field above to enable file downloads.',
7474
'Save changes in both Slack and here.',
@@ -92,6 +92,11 @@ export const slackWebhookTrigger: TriggerConfig = {
9292
type: 'string',
9393
description: 'Type of Slack event (e.g., app_mention, message)',
9494
},
95+
subtype: {
96+
type: 'string',
97+
description:
98+
'Message subtype (e.g., channel_join, channel_leave, bot_message, file_share). Null for regular user messages',
99+
},
95100
channel: {
96101
type: 'string',
97102
description: 'Slack channel ID where the event occurred',
@@ -100,6 +105,11 @@ export const slackWebhookTrigger: TriggerConfig = {
100105
type: 'string',
101106
description: 'Human-readable channel name',
102107
},
108+
channel_type: {
109+
type: 'string',
110+
description:
111+
'Type of channel (e.g., channel, group, im, mpim). Useful for distinguishing DMs from public channels',
112+
},
103113
user: {
104114
type: 'string',
105115
description: 'User ID who triggered the event',
@@ -108,6 +118,10 @@ export const slackWebhookTrigger: TriggerConfig = {
108118
type: 'string',
109119
description: 'Username who triggered the event',
110120
},
121+
bot_id: {
122+
type: 'string',
123+
description: 'Bot ID if the message was sent by a bot. Null for human users',
124+
},
111125
text: {
112126
type: 'string',
113127
description: 'Message text content',

0 commit comments

Comments
 (0)