You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[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.
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:
is_channel_allowed(channel_id) — group/channel allowlist from config.
is_user_allowed(user_id) — user allowlist from allowed_users / TELEGRAM_ALLOWED_USERS.
UnknownUserHandler.handle() — if user not explicitly allowed, offer DM pairing code or reject per unknown_user_policy.
Group policy — mention_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:
Extracts text (or transcribes voice).
Resolves which agent to use from gateway routing rules.
#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:
→ 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)
[BUG][Security] Gateway Telegram polling path bypasses
allowed_users, pairing, and group policyLabels:
bug,security,gateway,telegramOverview
When Telegram bots run through
praisonai gateway start, inbound messages take a different code path than when bots run viapraisonai 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 toagent.chat().Issue #1494 (closed) fixed loading
allowed_usersfromgateway.yamlintoBotConfig. That made the config correct but did not wire enforcement into the gateway's custom Telegram handler. Operators who configureTELEGRAM_ALLOWED_USERSduring 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:
If the operator did configure allowlists in
.envandgateway.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
allowed_userssends DMUnknownUserHandlersends pairing codeThere 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:#FFB6C1Why 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 usingApplication.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 startruns a Telegram bot, every inbound message passes through:is_channel_allowed(channel_id)— group/channel allowlist from config.is_user_allowed(user_id)— user allowlist fromallowed_users/TELEGRAM_ALLOWED_USERS.UnknownUserHandler.handle()— if user not explicitly allowed, offer DM pairing code or reject perunknown_user_policy.mention_required/group_policy: mention_onlygates 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 inlinehandle_message(update, context)that: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
BotConfigobject does contain the correctallowed_userslist (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:
@mervincfo_botcan query financial knowledge tools.praisonai onboardand entered their Telegram user ID believe they restricted access — they have not.Relationship to closed #1494
#1494 identified that
allowed_userswas dropped when constructingBotConfigin the gateway (BotConfig(token=token)with no allowlist). That was fixed — config now flows:→ 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: truein config, gateway must passpairing_storeintoUnknownUserHandlerthe same way standalone bots do (#1502 pairing work).3. Tests
Extend
tests/unit/gateway/test_channel_allowlist.pyto cover the polling handler path, not just config round-trip.Acceptance criteria
allowed_users: ["42"]— user99gets no agent response; user42doesgroup_policy/mention_required