Telegram Webhook Controller Lacks Signature Verification
Telegram webhook controller accepts arbitrary POST requests without cryptographic verification of origin. Attacker-controlled `update_id` value bypasses anti-replay store entirely, enabling account hijacking via forged webhook payloads when the controller is enabled.
Description
The Telegram webhook controller is currently disabled by a hardcoded boolean, but when enabled it will accept arbitrary POST requests without any cryptographic verification of the request origin. Telegram's Bot API supports a secret_token parameter (set during setWebhook) that is sent in the X-Telegram-Bot-Api-Secret-Token header, this controller does not validate it.
At telegram-webhook-handler.controller.ts#L11-L12, the only protection is a compile-time constant:
// eslint-disable-next-line @typescript-eslint/consistent-type-assertionsconst controllerBlocked = true as boolean; // cast to boolean to prevent dead-code elimination, designed to be toggled
At L14, L27-L36, the controller has no @UseGuards decorator and no signature/token verification:
@Controller()// L14, no guard decoratorexport class TelegramWebhookHandlerController {// ...@Post(buildLendingApiPath(lendingApiEndpoint.telegramWebhook, undefined))// L27, publicly accessible POST endpointpublic async processWebhook(@Body(new ValidateAndStopAtFirstErrorPipe()) update: TelegramUpdate): Promise<void> {if (controllerBlocked) {throw new ForbiddenException();// L30, only defense: a toggleable boolean}await this.telegramWebhookAntiReplayStore.executeOnlyOnce(// L33, anti-replay prevents duplicate update_id() => this.processTelegramUpdate(update),// but does NOT prevent forged payloadsupdate.update_id.toString(),// L35, attacker controls update_id value);}}
The anti-replay store at L33-L35 is keyed by update_id, which is an integer the attacker fully controls in the forged payload. By using unique update_id values, the attacker bypasses anti-replay entirely.
The handler at L39-L52 parses the update and dispatches it. The TelegramAccountService.handleTelegramUpdate() at telegram-account.service.ts#L99-L107 processes the /start command to link Telegram accounts:
public async handleTelegramUpdate(update: ProcessedTelegramUpdate): Promise<void> {if (!update.isCommand || !update.command) { return; }if (update.command === "start" && update.commandArgument) {await this.linkAccount(update.chatId, update.commandArgument, update.username);// L105, links attacker's chatId using forged authCode}}
The linkAccount method validates the auth code against the database, then creates the account link and sends a confirmation message through the bot.
Vulnerable scenario:
- The
controllerBlockedconstant is toggled tofalse(it is explicitly designed for this, theas booleancast prevents TypeScript dead-code elimination, indicating the team intends to enable it). - An attacker discovers the webhook URL (e.g., via the Swagger documentation exposed in production, or by guessing the lending API path convention).
- The attacker sends a forged POST request:
{"update_id": 999999,"message": {"message_id": 1,"from": { "id": 12345, "is_bot": false, "first_name": "Attacker", "username": "attacker" },"chat": { "id": 67890, "type": "private" },"date": 1700000000,"text": "/start VALID_AUTH_CODE_HERE"}}
If the attacker has intercepted or guessed a valid auth code (16-character random string, valid for 30 minutes per telegram-account.service.ts#L36-L37), the linkAccount method binds the attacker's chatId (67890) to the victim's wallet address.
Even without a valid auth code, the attacker can enumerate auth codes via brute-force, the endpoint has no rate limiting (@Throttle decorator is absent), and the error responses differentiate between "invalid" and "expired" codes (TelegramAuthCodeInvalidError vs TelegramAuthCodeExpiredError), enabling timing-based enumeration.
Impact
Account Hijacking. An attacker can link their Telegram account to a victim's wallet address by forging a webhook payload with a valid auth code, intercepting all future liquidation alerts and position notifications.
Information Disclosure. Liquidation alerts contain position-specific data (prices, leverage levels, market tickers) that enables front-running or informed trading against the victim's positions.
Auth Code Enumeration. The absence of rate limiting on the webhook endpoint and differentiated error responses enable brute-force enumeration of active auth codes within their 30-minute validity window.
Recommendation
Implement Telegram's webhook secret token verification: set a secret_token during the setWebhook API call and validate the X-Telegram-Bot-Api-Secret-Token header in a guard before processing the payload.
Add rate limiting (@Throttle) to the webhook endpoint.
Resolution
Accepted by team. Endpoint is double-blocked in every environment via DeprecatedEndpointGuard and a module-scope controllerBlocked flag (PR #3721).

