F-2026-0005·missing-signature-verification

Telegram Webhook Controller Lacks Signature Verification

Acknowledgedpentestbackendapigithub.com/bloom-art/api
TL;DR

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.

Severity
MEDIUM
Impact
HIGH
Likelihood
LOW
Method
MManual review
CAT.
Complexity
MEDIUM
Exploitability
MEDIUM
02Section · Description

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:

typescript
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const 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:

typescript
@Controller()
// L14, no guard decorator
export class TelegramWebhookHandlerController {
// ...
@Post(buildLendingApiPath(lendingApiEndpoint.telegramWebhook, undefined))
// L27, publicly accessible POST endpoint
public 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 payloads
update.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:

typescript
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:

  1. The controllerBlocked constant is toggled to false (it is explicitly designed for this, the as boolean cast prevents TypeScript dead-code elimination, indicating the team intends to enable it).
  2. An attacker discovers the webhook URL (e.g., via the Swagger documentation exposed in production, or by guessing the lending API path convention).
  3. The attacker sends a forged POST request:
code
{
"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.

03Section · Impact

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.

04Section · Recommendation

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.

05Section · Resolution

Resolution

Accepted by team. Endpoint is double-blocked in every environment via DeprecatedEndpointGuard and a module-scope controllerBlocked flag (PR #3721).

F-2026-0005

oog
zealynx

Smart Contract Security Digest

Monthly exploit breakdowns, audit checklists, and DeFi security research — straight to your inbox

© 2026 Zealynx