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:
waleed
2026-06-10 08:37:05 -07:00
parent b8c7f3d496
commit 3ed97a440b
2 changed files with 90 additions and 6 deletions
@@ -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()
})
})
+28 -6
View File
@@ -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 &&