mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-19 09:30:49 +08:00
fix(telegram): keep legacy webhooks working via Telegram source-IP fallback
The secret-token check rejected every webhook registered before secret_token support, breaking live triggers until re-saved. Fall back to verifying the request originates from Telegram's published webhook IP ranges when no secret is configured, so existing triggers keep firing with no re-save or migration while forged updates from arbitrary hosts are still rejected. Webhooks with a registered secret continue to use strict constant-time token verification.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { requestUtilsMockFns } from '@sim/testing'
|
||||
import { NextRequest } from 'next/server'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { telegramHandler } from '@/lib/webhooks/providers/telegram'
|
||||
|
||||
function reqWithHeaders(headers: Record<string, string>): NextRequest {
|
||||
@@ -7,7 +8,11 @@ function reqWithHeaders(headers: Record<string, string>): NextRequest {
|
||||
}
|
||||
|
||||
describe('Telegram webhook provider', () => {
|
||||
it('verifyAuth rejects when secretToken is not configured', () => {
|
||||
beforeEach(() => {
|
||||
requestUtilsMockFns.mockGetClientIp.mockReturnValue('203.0.113.7')
|
||||
})
|
||||
|
||||
it('verifyAuth rejects an unconfigured webhook when the source IP is not Telegram', () => {
|
||||
const res = telegramHandler.verifyAuth!({
|
||||
request: reqWithHeaders({ 'x-telegram-bot-api-secret-token': 'anything' }),
|
||||
rawBody: '{}',
|
||||
@@ -19,6 +24,32 @@ describe('Telegram webhook provider', () => {
|
||||
expect((res as { status?: number })?.status).toBe(401)
|
||||
})
|
||||
|
||||
it('verifyAuth accepts a legacy webhook (no secret) from a Telegram source IP', () => {
|
||||
requestUtilsMockFns.mockGetClientIp.mockReturnValue('149.154.167.197')
|
||||
const res = telegramHandler.verifyAuth!({
|
||||
request: reqWithHeaders({}),
|
||||
rawBody: '{}',
|
||||
requestId: 't1b',
|
||||
providerConfig: {},
|
||||
webhook: {},
|
||||
workflow: {},
|
||||
})
|
||||
expect(res).toBeNull()
|
||||
})
|
||||
|
||||
it('verifyAuth rejects a legacy webhook (no secret) when the source IP is unknown', () => {
|
||||
requestUtilsMockFns.mockGetClientIp.mockReturnValue('unknown')
|
||||
const res = telegramHandler.verifyAuth!({
|
||||
request: reqWithHeaders({}),
|
||||
rawBody: '{}',
|
||||
requestId: 't1c',
|
||||
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({}),
|
||||
|
||||
@@ -3,7 +3,9 @@ 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 * as ipaddr from 'ipaddr.js'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getClientIp } from '@/lib/core/utils/request'
|
||||
import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils'
|
||||
import type {
|
||||
AuthContext,
|
||||
@@ -17,28 +19,61 @@ import type {
|
||||
|
||||
const logger = createLogger('WebhookProvider:Telegram')
|
||||
|
||||
/**
|
||||
* Telegram's published source ranges for webhook delivery.
|
||||
* @see https://core.telegram.org/bots/webhooks
|
||||
*/
|
||||
const TELEGRAM_WEBHOOK_CIDRS: ReadonlyArray<readonly [string, number]> = [
|
||||
['149.154.160.0', 20],
|
||||
['91.108.4.0', 22],
|
||||
] as const
|
||||
|
||||
/** Whether a client IP falls inside Telegram's documented webhook source ranges. */
|
||||
function isTelegramWebhookIp(ip: string): boolean {
|
||||
if (!ip || ip === 'unknown' || !ipaddr.isValid(ip)) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
const addr = ipaddr.process(ip)
|
||||
if (addr.kind() !== 'ipv4') {
|
||||
return false
|
||||
}
|
||||
return TELEGRAM_WEBHOOK_CIDRS.some(([network, prefix]) =>
|
||||
addr.match([ipaddr.parse(network), prefix])
|
||||
)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const telegramHandler: WebhookProviderHandler = {
|
||||
verifyAuth({ request, requestId, providerConfig }: AuthContext): NextResponse | null {
|
||||
const secretToken = (providerConfig.secretToken as string | undefined)?.trim()
|
||||
if (!secretToken) {
|
||||
|
||||
if (secretToken) {
|
||||
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
|
||||
}
|
||||
|
||||
const clientIp = getClientIp(request)
|
||||
if (!isTelegramWebhookIp(clientIp)) {
|
||||
logger.warn(
|
||||
`[${requestId}] Telegram webhook missing secretToken in providerConfig — rejecting request. Re-save the trigger so a secret token can be registered with Telegram.`
|
||||
`[${requestId}] Telegram webhook without a registered secret token rejected — source IP is not in Telegram's published ranges. Re-save the trigger to enable secret-token verification.`,
|
||||
{ clientIp }
|
||||
)
|
||||
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 new NextResponse('Unauthorized - Untrusted Telegram webhook source', { status: 401 })
|
||||
}
|
||||
|
||||
return null
|
||||
|
||||
Reference in New Issue
Block a user