Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion apps/sim/lib/webhooks/providers/airtable.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { db } from '@sim/db'
import { account, webhook } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { safeCompare } from '@sim/security/compare'
import { hmacSha256Base64 } from '@sim/security/hmac'
import { eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { validateAirtableId } from '@/lib/core/security/input-validation'
import { getBaseUrl } from '@/lib/core/utils/urls'
import {
Expand All @@ -10,6 +13,7 @@ import {
getProviderConfig,
} from '@/lib/webhooks/provider-subscription-utils'
import type {
AuthContext,
DeleteSubscriptionContext,
FormatInputContext,
SubscriptionContext,
Expand Down Expand Up @@ -437,7 +441,34 @@ async function fetchAndProcessAirtablePayloads(
}
}

function validateAirtableSignature(webhookSecret: string, mac: string, rawBody: string): boolean {
const prefix = 'hmac-sha256='
if (!mac.startsWith(prefix)) return false
const provided = mac.slice(prefix.length)
const secretBuffer = Buffer.from(webhookSecret, 'base64')
const computed = hmacSha256Base64(rawBody, secretBuffer)
return safeCompare(provided, computed)
}

export const airtableHandler: WebhookProviderHandler = {
verifyAuth({ request, rawBody, requestId, providerConfig }: AuthContext) {
const webhookSecret = providerConfig.webhookSecret as string | undefined
if (!webhookSecret) return null

const mac = request.headers.get('x-airtable-content-mac')
if (!mac) {
logger.warn(`[${requestId}] Airtable webhook missing X-Airtable-Content-Mac header`)
return new NextResponse('Unauthorized - Missing Airtable signature', { status: 401 })
}

if (!validateAirtableSignature(webhookSecret, mac, rawBody)) {
logger.warn(`[${requestId}] Airtable signature verification failed`)
return new NextResponse('Unauthorized - Invalid Airtable signature', { status: 401 })
}

return null
},

async createSubscription({
webhook: webhookRecord,
workflow,
Expand Down Expand Up @@ -554,7 +585,12 @@ export const airtableHandler: WebhookProviderHandler = {
airtableWebhookId: responseBody.id,
}
)
return { providerConfigUpdates: { externalId: responseBody.id } }
return {
providerConfigUpdates: {
externalId: responseBody.id,
...(responseBody.macSecretBase64 ? { webhookSecret: responseBody.macSecretBase64 } : {}),
},
}
} catch (error: unknown) {
const err = error as Error
logger.error(
Expand Down
34 changes: 34 additions & 0 deletions apps/sim/lib/webhooks/providers/hubspot.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { createHash } from 'node:crypto'
import { createLogger } from '@sim/logger'
import { safeCompare } from '@sim/security/compare'
import { NextResponse } from 'next/server'
import type {
AuthContext,
EventMatchContext,
FormatInputContext,
FormatInputResult,
Expand All @@ -8,7 +12,37 @@ import type {

const logger = createLogger('WebhookProvider:HubSpot')

function validateHubSpotSignature(
clientSecret: string,
signature: string,
rawBody: string
): boolean {
// HubSpot v1: SHA256(clientSecret + rawBody) → hex
const hash = createHash('sha256')
.update(clientSecret + rawBody, 'utf8')
.digest('hex')
return safeCompare(hash, signature)
}
Comment thread
waleedlatif1 marked this conversation as resolved.
Outdated

export const hubspotHandler: WebhookProviderHandler = {
verifyAuth({ request, rawBody, requestId, providerConfig }: AuthContext) {
const clientSecret = providerConfig.clientSecret as string | undefined
if (!clientSecret) return null

const signature = request.headers.get('x-hubspot-signature')
if (!signature) {
logger.warn(`[${requestId}] HubSpot webhook missing X-HubSpot-Signature header`)
return new NextResponse('Unauthorized - Missing HubSpot signature', { status: 401 })
}

if (!validateHubSpotSignature(clientSecret, signature, rawBody)) {
logger.warn(`[${requestId}] HubSpot signature verification failed`)
return new NextResponse('Unauthorized - Invalid HubSpot signature', { status: 401 })
}

return null
},

async matchEvent({ webhook, workflow, body, requestId, providerConfig }: EventMatchContext) {
const triggerId = providerConfig.triggerId as string | undefined

Expand Down
34 changes: 33 additions & 1 deletion apps/sim/lib/webhooks/providers/webflow.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { createLogger } from '@sim/logger'
import { safeCompare } from '@sim/security/compare'
import { hmacSha256Hex } from '@sim/security/hmac'
import { NextResponse } from 'next/server'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getCredentialOwner, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils'
import type {
AuthContext,
DeleteSubscriptionContext,
EventFilterContext,
FormatInputContext,
Expand All @@ -15,7 +19,30 @@ import { getOAuthToken, refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/

const logger = createLogger('WebhookProvider:Webflow')

function validateWebflowSignature(secret: string, signature: string, rawBody: string): boolean {
const computed = hmacSha256Hex(rawBody, secret)
return safeCompare(computed, signature)
}
Comment thread
waleedlatif1 marked this conversation as resolved.

export const webflowHandler: WebhookProviderHandler = {
verifyAuth({ request, rawBody, requestId, providerConfig }: AuthContext) {
const secret = providerConfig.webhookSecret as string | undefined
Comment thread
waleedlatif1 marked this conversation as resolved.
if (!secret) return null

const signature = request.headers.get('x-webflow-signature')
Comment thread
waleedlatif1 marked this conversation as resolved.
if (!signature) {
logger.warn(`[${requestId}] Webflow webhook missing X-Webflow-Signature header`)
return new NextResponse('Unauthorized - Missing Webflow signature', { status: 401 })
}

if (!validateWebflowSignature(secret, signature, rawBody)) {
logger.warn(`[${requestId}] Webflow signature verification failed`)
return new NextResponse('Unauthorized - Invalid Webflow signature', { status: 401 })
}

return null
},
Comment thread
waleedlatif1 marked this conversation as resolved.

async createSubscription({
webhook: webhookRecord,
workflow,
Expand Down Expand Up @@ -132,7 +159,12 @@ export const webflowHandler: WebhookProviderHandler = {
}
)

return { providerConfigUpdates: { externalId: responseBody.id || responseBody._id } }
return {
providerConfigUpdates: {
externalId: responseBody.id || responseBody._id,
...(responseBody.secretToken ? { webhookSecret: responseBody.secretToken } : {}),
},
}
Comment thread
waleedlatif1 marked this conversation as resolved.
} catch (error: unknown) {
const err = error as Error
logger.error(
Expand Down
Loading