Skip to content
Merged
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
Prev Previous commit
Next Next commit
fix(triggers): harden Zoom webhook security per PR review
- verifyAuth now fails closed (401) when secretToken is missing
- handleChallenge DB query filters by provider='zoom' to avoid cross-provider leaks
- handleChallenge verifies x-zm-signature before responding to prevent HMAC oracle
  • Loading branch information
waleedlatif1 committed Apr 6, 2026
commit 4fd970a7a05d6f149133e5be9097c424993d3ea4
22 changes: 19 additions & 3 deletions apps/sim/lib/webhooks/providers/zoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ export const zoomHandler: WebhookProviderHandler = {
verifyAuth({ request, rawBody, requestId, providerConfig }: AuthContext) {
const secretToken = providerConfig.secretToken as string | undefined
if (!secretToken) {
return null
logger.warn(
`[${requestId}] Zoom webhook missing secretToken in providerConfig — rejecting request`
)
return new NextResponse('Unauthorized - Zoom secret token not configured', { status: 401 })
}
Comment thread
waleedlatif1 marked this conversation as resolved.

const signature = request.headers.get('x-zm-signature')
Expand Down Expand Up @@ -98,7 +101,7 @@ export const zoomHandler: WebhookProviderHandler = {
* Zoom sends an `endpoint.url_validation` event with a `plainToken` that must
* be hashed with the app's secret token and returned alongside the original token.
*/
async handleChallenge(body: unknown, _request: NextRequest, requestId: string, path: string) {
async handleChallenge(body: unknown, request: NextRequest, requestId: string, path: string) {
const obj = body as Record<string, unknown> | null
if (obj?.event !== 'endpoint.url_validation') {
return null
Expand All @@ -118,7 +121,9 @@ export const zoomHandler: WebhookProviderHandler = {
const webhooks = await db
.select()
.from(webhook)
.where(and(eq(webhook.path, path), eq(webhook.isActive, true)))
.where(
and(eq(webhook.path, path), eq(webhook.provider, 'zoom'), eq(webhook.isActive, true))
)
Comment thread
waleedlatif1 marked this conversation as resolved.
if (webhooks.length > 0) {
const config = webhooks[0].providerConfig as Record<string, unknown> | null
secretToken = (config?.secretToken as string) || ''
Expand All @@ -135,6 +140,17 @@ export const zoomHandler: WebhookProviderHandler = {
return null
}

// Verify the challenge request's signature to prevent HMAC oracle attacks
const signature = request.headers.get('x-zm-signature')
const timestamp = request.headers.get('x-zm-request-timestamp')
if (signature && timestamp) {
const rawBody = JSON.stringify(body)
if (!validateZoomSignature(secretToken, signature, timestamp, rawBody)) {
logger.warn(`[${requestId}] Zoom challenge request failed signature verification`)
return null
}
}
Comment thread
waleedlatif1 marked this conversation as resolved.

const hashForValidate = crypto
.createHmac('sha256', secretToken)
.update(plainToken)
Expand Down