mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-19 09:30:49 +08:00
fix(telegram): verify X-Telegram-Bot-Api-Secret-Token on inbound webhooks
Telegram triggers accepted any forged update from anyone who knew the webhook URL path: verifyAuth was a no-op that always returned null, and setWebhook registered no secret_token. Generate a per-webhook secret in createSubscription, register it with Telegram as secret_token, and persist it to providerConfig. verifyAuth now fails closed — rejects when no token is configured, when the X-Telegram-Bot-Api-Secret-Token header is absent, or when it does not match via constant-time safeCompare.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import { NextRequest } from 'next/server'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { telegramHandler } from '@/lib/webhooks/providers/telegram'
|
||||
|
||||
function reqWithHeaders(headers: Record<string, string>): NextRequest {
|
||||
return new NextRequest('http://localhost/test', { headers })
|
||||
}
|
||||
|
||||
describe('Telegram webhook provider', () => {
|
||||
it('verifyAuth rejects when secretToken is not configured', () => {
|
||||
const res = telegramHandler.verifyAuth!({
|
||||
request: reqWithHeaders({ 'x-telegram-bot-api-secret-token': 'anything' }),
|
||||
rawBody: '{}',
|
||||
requestId: 't1',
|
||||
providerConfig: {},
|
||||
webhook: {},
|
||||
workflow: {},
|
||||
})
|
||||
expect((res as { status?: number })?.status).toBe(401)
|
||||
})
|
||||
|
||||
it('verifyAuth rejects when the secret token header is missing', () => {
|
||||
const res = telegramHandler.verifyAuth!({
|
||||
request: reqWithHeaders({}),
|
||||
rawBody: '{}',
|
||||
requestId: 't2',
|
||||
providerConfig: { secretToken: 'super-secret' },
|
||||
webhook: {},
|
||||
workflow: {},
|
||||
})
|
||||
expect((res as { status?: number })?.status).toBe(401)
|
||||
})
|
||||
|
||||
it('verifyAuth rejects when the secret token does not match', () => {
|
||||
const res = telegramHandler.verifyAuth!({
|
||||
request: reqWithHeaders({ 'x-telegram-bot-api-secret-token': 'wrong' }),
|
||||
rawBody: '{}',
|
||||
requestId: 't3',
|
||||
providerConfig: { secretToken: 'super-secret' },
|
||||
webhook: {},
|
||||
workflow: {},
|
||||
})
|
||||
expect((res as { status?: number })?.status).toBe(401)
|
||||
})
|
||||
|
||||
it('verifyAuth accepts a matching secret token', () => {
|
||||
const res = telegramHandler.verifyAuth!({
|
||||
request: reqWithHeaders({ 'x-telegram-bot-api-secret-token': 'super-secret' }),
|
||||
rawBody: '{}',
|
||||
requestId: 't4',
|
||||
providerConfig: { secretToken: 'super-secret' },
|
||||
webhook: {},
|
||||
workflow: {},
|
||||
})
|
||||
expect(res).toBeNull()
|
||||
})
|
||||
|
||||
it('extractIdempotencyId keys on update_id', () => {
|
||||
expect(telegramHandler.extractIdempotencyId!({ update_id: 42 })).toBe('telegram:42')
|
||||
expect(telegramHandler.extractIdempotencyId!({})).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,9 @@
|
||||
import { db, webhook, workflowDeploymentVersion } from '@sim/db'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { safeCompare } from '@sim/security/compare'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { and, eq, isNull, ne } from 'drizzle-orm'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils'
|
||||
import type {
|
||||
AuthContext,
|
||||
@@ -15,13 +18,29 @@ import type {
|
||||
const logger = createLogger('WebhookProvider:Telegram')
|
||||
|
||||
export const telegramHandler: WebhookProviderHandler = {
|
||||
verifyAuth({ request, requestId }: AuthContext) {
|
||||
const userAgent = request.headers.get('user-agent')
|
||||
if (!userAgent) {
|
||||
verifyAuth({ request, requestId, providerConfig }: AuthContext): NextResponse | null {
|
||||
const secretToken = (providerConfig.secretToken as string | undefined)?.trim()
|
||||
if (!secretToken) {
|
||||
logger.warn(
|
||||
`[${requestId}] Telegram webhook request has empty User-Agent header. This may be blocked by middleware.`
|
||||
`[${requestId}] Telegram webhook missing secretToken in providerConfig — rejecting request. Re-save the trigger so a secret token can be registered with Telegram.`
|
||||
)
|
||||
return new NextResponse(
|
||||
'Unauthorized - Telegram webhook secret token is not configured. Re-save the trigger so a webhook can be registered.',
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const providedToken = request.headers.get('x-telegram-bot-api-secret-token')
|
||||
if (!providedToken) {
|
||||
logger.warn(`[${requestId}] Telegram webhook missing secret token header — rejecting request`)
|
||||
return new NextResponse('Unauthorized - Missing Telegram secret token', { status: 401 })
|
||||
}
|
||||
|
||||
if (!safeCompare(providedToken, secretToken)) {
|
||||
logger.warn(`[${requestId}] Telegram secret token verification failed`)
|
||||
return new NextResponse('Unauthorized - Invalid Telegram secret token', { status: 401 })
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
|
||||
@@ -125,6 +144,9 @@ export const telegramHandler: WebhookProviderHandler = {
|
||||
const notificationUrl = getNotificationUrl(ctx.webhook)
|
||||
const telegramApiUrl = `https://api.telegram.org/bot${botToken}/setWebhook`
|
||||
|
||||
const existingSecretToken = (config.secretToken as string | undefined)?.trim()
|
||||
const secretToken = existingSecretToken || generateId()
|
||||
|
||||
try {
|
||||
const telegramResponse = await fetch(telegramApiUrl, {
|
||||
method: 'POST',
|
||||
@@ -132,7 +154,7 @@ export const telegramHandler: WebhookProviderHandler = {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'TelegramBot/1.0',
|
||||
},
|
||||
body: JSON.stringify({ url: notificationUrl }),
|
||||
body: JSON.stringify({ url: notificationUrl, secret_token: secretToken }),
|
||||
})
|
||||
|
||||
const responseBody = await telegramResponse.json()
|
||||
@@ -156,7 +178,7 @@ export const telegramHandler: WebhookProviderHandler = {
|
||||
logger.info(
|
||||
`[${ctx.requestId}] Successfully created Telegram webhook for webhook ${ctx.webhook.id}`
|
||||
)
|
||||
return {}
|
||||
return { providerConfigUpdates: { secretToken } }
|
||||
} catch (error: unknown) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
|
||||
Reference in New Issue
Block a user