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:
', 'Go to "Basic Information", find the "Signing Secret", and paste it in the field above.', 'Go to "OAuth & Permissions" and add bot token scopes:
', - 'Go to "Event Subscriptions":
', + 'Go to "Event Subscriptions":
', 'Go to "Install App" in the left sidebar and install the app into your desired Slack workspace and channel.', 'Copy the "Bot User OAuth Token" (starts with xoxb-) 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',