Skip to content

Commit 3a33754

Browse files
committed
fix(security): correct webhook signature algorithms for Webflow and Airtable
- Webflow: sign `${timestamp}:${rawBody}` per official docs (was rawBody only), capture `secretKey` not `secretToken` from create response, add 5-minute replay protection via X-Webflow-Timestamp - Airtable: use hmacSha256Hex for hmac-sha256=<hex> format (was Base64)
1 parent 6e7cc49 commit 3a33754

2 files changed

Lines changed: 24 additions & 6 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { db } from '@sim/db'
22
import { account, webhook } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { safeCompare } from '@sim/security/compare'
5-
import { hmacSha256Base64 } from '@sim/security/hmac'
5+
import { hmacSha256Hex } from '@sim/security/hmac'
66
import { eq } from 'drizzle-orm'
77
import { NextResponse } from 'next/server'
88
import { validateAirtableId } from '@/lib/core/security/input-validation'
@@ -446,7 +446,7 @@ function validateAirtableSignature(webhookSecret: string, mac: string, rawBody:
446446
if (!mac.startsWith(prefix)) return false
447447
const provided = mac.slice(prefix.length)
448448
const secretBuffer = Buffer.from(webhookSecret, 'base64')
449-
const computed = hmacSha256Base64(rawBody, secretBuffer)
449+
const computed = hmacSha256Hex(rawBody, secretBuffer)
450450
return safeCompare(provided, computed)
451451
}
452452

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

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,41 @@ import { getOAuthToken, refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/
1919

2020
const logger = createLogger('WebhookProvider:Webflow')
2121

22-
function validateWebflowSignature(secret: string, signature: string, rawBody: string): boolean {
23-
const computed = hmacSha256Hex(rawBody, secret)
22+
function validateWebflowSignature(
23+
secret: string,
24+
signature: string,
25+
timestamp: string,
26+
rawBody: string
27+
): boolean {
28+
const computed = hmacSha256Hex(`${timestamp}:${rawBody}`, secret)
2429
return safeCompare(computed, signature)
2530
}
2631

32+
const FIVE_MINUTES_MS = 5 * 60 * 1000
33+
2734
export const webflowHandler: WebhookProviderHandler = {
2835
verifyAuth({ request, rawBody, requestId, providerConfig }: AuthContext) {
2936
const secret = providerConfig.webhookSecret as string | undefined
3037
if (!secret) return null
3138

39+
const timestamp = request.headers.get('x-webflow-timestamp')
40+
if (!timestamp) {
41+
logger.warn(`[${requestId}] Webflow webhook missing X-Webflow-Timestamp header`)
42+
return new NextResponse('Unauthorized - Missing Webflow timestamp', { status: 401 })
43+
}
44+
3245
const signature = request.headers.get('x-webflow-signature')
3346
if (!signature) {
3447
logger.warn(`[${requestId}] Webflow webhook missing X-Webflow-Signature header`)
3548
return new NextResponse('Unauthorized - Missing Webflow signature', { status: 401 })
3649
}
3750

38-
if (!validateWebflowSignature(secret, signature, rawBody)) {
51+
if (Math.abs(Date.now() - Number(timestamp)) > FIVE_MINUTES_MS) {
52+
logger.warn(`[${requestId}] Webflow webhook timestamp too old, possible replay attack`)
53+
return new NextResponse('Unauthorized - Webflow timestamp expired', { status: 401 })
54+
}
55+
56+
if (!validateWebflowSignature(secret, signature, timestamp, rawBody)) {
3957
logger.warn(`[${requestId}] Webflow signature verification failed`)
4058
return new NextResponse('Unauthorized - Invalid Webflow signature', { status: 401 })
4159
}
@@ -162,7 +180,7 @@ export const webflowHandler: WebhookProviderHandler = {
162180
return {
163181
providerConfigUpdates: {
164182
externalId: responseBody.id || responseBody._id,
165-
...(responseBody.secretToken ? { webhookSecret: responseBody.secretToken } : {}),
183+
...(responseBody.secretKey ? { webhookSecret: responseBody.secretKey } : {}),
166184
},
167185
}
168186
} catch (error: unknown) {

0 commit comments

Comments
 (0)