Skip to content

[BUG][Security] Gateway Telegram polling path bypasses allowed_users, pairing, and group policy #1747

@Dhivya-Bharathy

Description

@Dhivya-Bharathy

[BUG][Security] Gateway Telegram polling path bypasses allowed_users, pairing, and group policy

Labels: bug, security, gateway, telegram


Overview

When Telegram bots run through praisonai gateway start, inbound messages take a different code path than when bots run via praisonai bot start. The standalone bot path enforces access control (allowlists, DM pairing, group policies). The gateway polling path does not — it routes every message straight to agent.chat().

Issue #1494 (closed) fixed loading allowed_users from gateway.yaml into BotConfig. That made the config correct but did not wire enforcement into the gateway's custom Telegram handler. Operators who configure TELEGRAM_ALLOWED_USERS during onboard believe their bots are restricted. In gateway mode, they are not.


What the user sees

Startup warning (misleading)

Every gateway start without an allowlist logs:

WARNING Channel 'telegram_cfo' has no allowed_users — bot accepts messages from everyone.
Re-run `praisonai onboard` to configure.

If the operator did configure allowlists in .env and gateway.yaml, the warning may disappear — but enforcement still does not run in the gateway Telegram polling handler. The bot accepts messages from any Telegram user ID.

Behavioral symptoms

Scenario Expected (standalone bot) Actual (gateway)
User not in allowed_users sends DM Message dropped or pairing code offered Agent responds normally
Unauthorized user in group Ignored unless mentioned + allowed Agent may respond
Pairing flow for unknown user UnknownUserHandler sends pairing code Never runs
Operator restricts via onboard Effective immediately Ineffective in gateway mode

There is no error message — the failure mode is silent over-permission, which is worse than a crash.


Architecture — two Telegram message paths

flowchart LR
    subgraph Standalone["praisonai bot start"]
        TG1["Telegram update"] --> HB["TelegramBot.handle_message()"]
        HB --> CHK1["is_channel_allowed()"]
        CHK1 --> CHK2["is_user_allowed()"]
        CHK2 --> CHK3["UnknownUserHandler (pairing)"]
        CHK3 --> AGENT1["agent.chat()"]
    end

    subgraph Gateway["praisonai gateway start"]
        TG2["Telegram update"] --> POLL["_start_telegram_bot_polling()"]
        POLL --> ROUTE["resolve agent from routing rules"]
        ROUTE --> TYPING["send_action typing"]
        TYPING --> AGENT2["bot._session.chat()"]
    end

    style CHK1 fill:#90EE90
    style CHK2 fill:#90EE90
    style CHK3 fill:#90EE90
    style ROUTE fill:#FFB6C1
    style AGENT2 fill:#FFB6C1
Loading

Why two paths exist

Gateway runs Telegram inside an existing asyncio event loop (uvicorn + WebSocket). The high-level TelegramBot.run_polling() tries to own its own loop, which conflicts with the gateway. The fix was _start_telegram_bot_polling() — a low-level PTB integration using Application.initialize() → start() → updater.start_polling().

That reimplementation copied routing, typing, and session chat — but not the security gate chain from TelegramBot.handle_message().


Security gate chain (standalone path — working)

When praisonai bot start runs a Telegram bot, every inbound message passes through:

  1. is_channel_allowed(channel_id) — group/channel allowlist from config.
  2. is_user_allowed(user_id) — user allowlist from allowed_users / TELEGRAM_ALLOWED_USERS.
  3. UnknownUserHandler.handle() — if user not explicitly allowed, offer DM pairing code or reject per unknown_user_policy.
  4. Group policymention_required / group_policy: mention_only gates group messages.

Only after all checks pass does the message reach handlers and agent.chat().


Gateway polling path (broken enforcement)

_start_telegram_bot_polling() defines an inline handle_message(update, context) that:

  1. Extracts text (or transcribes voice).
  2. Resolves which agent to use from gateway routing rules.
  3. Sends a one-shot typing indicator.
  4. Calls bot._session.chat(agent, user_id, message_text) directly.

Missing steps: is_channel_allowed, is_user_allowed, UnknownUserHandler, group mention checks, pairing store integration.

The BotConfig object does contain the correct allowed_users list (thanks to #1494). It is simply never consulted in this handler.


Real-world deployment context

A Hermes-style workforce runs three public-facing Telegram bots (CFO, Ops, Content) on one gateway. Without enforcement:

  • Anyone who discovers @mervincfo_bot can query financial knowledge tools.
  • Competitor or random user messages consume OpenAI quota.
  • Operators who ran praisonai onboard and entered their Telegram user ID believe they restricted access — they have not.

Relationship to closed #1494

#1494 identified that allowed_users was dropped when constructing BotConfig in the gateway (BotConfig(token=token) with no allowlist). That was fixed — config now flows:

channels:
  telegram_cfo:
    allowed_users: ${TELEGRAM_ALLOWED_USERS}

→ parsed into BotConfig(allowed_users=[...]) → startup warning if empty.

This issue is the remaining gap: config is loaded but not enforced in _start_telegram_bot_polling(). #1494 Phase 1 acceptance criteria included "daemon-run bot rejects user id 99, accepts 42" — that criterion is not met for gateway Telegram polling today.


Proposed fix

1. Extract shared inbound pipeline

Create process_inbound_telegram_message(bot, update, gateway_context) used by both:

  • TelegramBot.handle_message() (standalone)
  • _start_telegram_bot_polling() (gateway)

Pipeline order: channel allowlist → user allowlist → pairing → group policy → routing → chat.

2. Wire gateway pairing store

If pairing.enabled: true in config, gateway must pass pairing_store into UnknownUserHandler the same way standalone bots do (#1502 pairing work).

3. Tests

Extend tests/unit/gateway/test_channel_allowlist.py to cover the polling handler path, not just config round-trip.


Acceptance criteria

  • Gateway Telegram with allowed_users: ["42"] — user 99 gets no agent response; user 42 does
  • Unauthorized DM triggers pairing flow when pairing is enabled
  • Group messages respect group_policy / mention_required
  • Standalone bot path behavior unchanged (no regression)
  • Security gates run before any LLM call (no quota leak to unauthorized users)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsecurity

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions