diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts
index 70ece5a0e88..84d903c1754 100644
--- a/apps/sim/blocks/blocks/slack.ts
+++ b/apps/sim/blocks/blocks/slack.ts
@@ -1634,8 +1634,21 @@ Do not include any explanations, markdown formatting, or other text outside the
// Trigger outputs (when used as webhook trigger)
event_type: { type: 'string', description: 'Type of Slack event that triggered the workflow' },
+ subtype: {
+ type: 'string',
+ description:
+ 'Message subtype (e.g., channel_join, channel_leave, bot_message). Null for regular user messages',
+ },
channel_name: { type: 'string', description: 'Human-readable channel name' },
+ channel_type: {
+ type: 'string',
+ description: 'Type of channel (e.g., channel, group, im, mpim)',
+ },
user_name: { type: 'string', description: 'Username who triggered the event' },
+ bot_id: {
+ type: 'string',
+ description: 'Bot ID if the message was sent by a bot. Null for human users',
+ },
timestamp: { type: 'string', description: 'Message timestamp from the triggering event' },
thread_ts: {
type: 'string',
diff --git a/apps/sim/lib/webhooks/providers/slack.ts b/apps/sim/lib/webhooks/providers/slack.ts
index 1bcedd628b9..d645fd791ef 100644
--- a/apps/sim/lib/webhooks/providers/slack.ts
+++ b/apps/sim/lib/webhooks/providers/slack.ts
@@ -1,10 +1,13 @@
+import crypto from 'crypto'
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
+import { safeCompare } from '@/lib/core/security/encryption'
import {
secureFetchWithPinnedIP,
validateUrlWithDNS,
} from '@/lib/core/security/input-validation.server'
import type {
+ AuthContext,
FormatInputContext,
FormatInputResult,
WebhookProviderHandler,
@@ -177,6 +180,44 @@ async function fetchSlackMessageText(
}
}
+/** Maximum allowed timestamp skew (5 minutes) per Slack docs. */
+const SLACK_TIMESTAMP_MAX_SKEW = 300
+
+/**
+ * Validate Slack request signature using HMAC-SHA256.
+ * Basestring format: `v0:{timestamp}:{rawBody}`
+ * Signature header format: `v0={hex}`
+ */
+function validateSlackSignature(
+ signingSecret: string,
+ signature: string,
+ timestamp: string,
+ rawBody: string
+): boolean {
+ try {
+ if (!signingSecret || !signature || !rawBody) {
+ return false
+ }
+
+ if (!signature.startsWith('v0=')) {
+ logger.warn('Slack signature has invalid format (missing v0= prefix)')
+ return false
+ }
+
+ const providedSignature = signature.substring(3)
+ const basestring = `v0:${timestamp}:${rawBody}`
+ const computedHash = crypto
+ .createHmac('sha256', signingSecret)
+ .update(basestring, 'utf8')
+ .digest('hex')
+
+ return safeCompare(computedHash, providedSignature)
+ } catch (error) {
+ logger.error('Error validating Slack signature:', error)
+ return false
+ }
+}
+
/**
* Handle Slack verification challenges
*/
@@ -190,6 +231,44 @@ export function handleSlackChallenge(body: unknown): NextResponse | null {
}
export const slackHandler: WebhookProviderHandler = {
+ verifyAuth({ request, rawBody, requestId, providerConfig }: AuthContext) {
+ const signingSecret = providerConfig.signingSecret as string | undefined
+ if (!signingSecret) {
+ return null
+ }
+
+ const signature = request.headers.get('x-slack-signature')
+ const timestamp = request.headers.get('x-slack-request-timestamp')
+
+ if (!signature || !timestamp) {
+ logger.warn(`[${requestId}] Slack webhook missing signature or timestamp header`)
+ return new NextResponse('Unauthorized - Missing Slack signature', { status: 401 })
+ }
+
+ const now = Math.floor(Date.now() / 1000)
+ const parsedTimestamp = Number(timestamp)
+ if (Number.isNaN(parsedTimestamp)) {
+ logger.warn(`[${requestId}] Slack webhook timestamp is not a valid number`, { timestamp })
+ return new NextResponse('Unauthorized - Invalid timestamp', { status: 401 })
+ }
+ const skew = Math.abs(now - parsedTimestamp)
+ if (skew > SLACK_TIMESTAMP_MAX_SKEW) {
+ logger.warn(`[${requestId}] Slack webhook timestamp too old`, {
+ timestamp,
+ now,
+ skew,
+ })
+ return new NextResponse('Unauthorized - Request timestamp too old', { status: 401 })
+ }
+
+ if (!validateSlackSignature(signingSecret, signature, timestamp, rawBody)) {
+ logger.warn(`[${requestId}] Slack signature verification failed`)
+ return new NextResponse('Unauthorized - Invalid Slack signature', { status: 401 })
+ }
+
+ return null
+ },
+
handleChallenge(body: unknown) {
return handleSlackChallenge(body)
},
@@ -262,10 +341,13 @@ export const slackHandler: WebhookProviderHandler = {
input: {
event: {
event_type: eventType,
+ subtype: (rawEvent?.subtype as string) ?? '',
channel,
channel_name: '',
+ channel_type: (rawEvent?.channel_type as string) ?? '',
user: (rawEvent?.user as string) || '',
user_name: '',
+ bot_id: (rawEvent?.bot_id as string) ?? '',
text,
timestamp: messageTs,
thread_ts: (rawEvent?.thread_ts as string) || '',
diff --git a/apps/sim/triggers/slack/webhook.ts b/apps/sim/triggers/slack/webhook.ts
index 3f1bbe2c0f7..2fa8966ae63 100644
--- a/apps/sim/triggers/slack/webhook.ts
+++ b/apps/sim/triggers/slack/webhook.ts
@@ -68,7 +68,7 @@ export const slackWebhookTrigger: TriggerConfig = {
'If you don\'t have an app:
app_mentions:read - For viewing messages that tag your bot with an @chat:write - To send messages to channels your bot is a part offiles:read - To access files and images shared in messagesreactions:read - For listening to emoji reactions and fetching reacted-to message textapp_mention to listen to messages that mention your botreaction_added and/or reaction_removedapp_mention to listen to messages that mention your botmessage.channels. For DMs add message.im, for group DMs add message.mpim, for private channels add message.groupsreaction_added and/or reaction_removedxoxb-) and paste it in the Bot Token field above to enable file downloads.',
'Save changes in both Slack and here.',
@@ -92,6 +92,11 @@ export const slackWebhookTrigger: TriggerConfig = {
type: 'string',
description: 'Type of Slack event (e.g., app_mention, message)',
},
+ subtype: {
+ type: 'string',
+ description:
+ 'Message subtype (e.g., channel_join, channel_leave, bot_message, file_share). Null for regular user messages',
+ },
channel: {
type: 'string',
description: 'Slack channel ID where the event occurred',
@@ -100,6 +105,11 @@ export const slackWebhookTrigger: TriggerConfig = {
type: 'string',
description: 'Human-readable channel name',
},
+ channel_type: {
+ type: 'string',
+ description:
+ 'Type of channel (e.g., channel, group, im, mpim). Useful for distinguishing DMs from public channels',
+ },
user: {
type: 'string',
description: 'User ID who triggered the event',
@@ -108,6 +118,10 @@ export const slackWebhookTrigger: TriggerConfig = {
type: 'string',
description: 'Username who triggered the event',
},
+ bot_id: {
+ type: 'string',
+ description: 'Bot ID if the message was sent by a bot. Null for human users',
+ },
text: {
type: 'string',
description: 'Message text content',