Owner Telegram bot with an empty allowlist serves any sender as operator (open by default)
Version: ccteam 0.11.0 (3ed05d4), Linux x86_64, standalone install Severity: high in the common setup (fresh install, token pasted, no chat bound yet)
What happens
After pasting a @Botfather token in Settings › Access › Telegram and saving, the daemon starts polling that bot with no allowlist. In that state every Telegram user who finds the bot is treated as the operator: they can create sessions, send prompts, and (with the default bypassPermissions project settings, see the companion issue) run unattended shell and file operations on the daemon host.
The daemon knows this and logs it:
imd: the telegram bot has an EMPTY allowlist (open mode) — anyone who finds it is served as the operator; add your own chat id to close it
but nothing in the web UI says so. The Telegram card shows bound chats 0 and the hint "DM the bot to bind your chat", which reads as "the bot does nothing until you bind", i.e. the opposite of the real behavior. The Lark card next to it explicitly says "empty allowlist = fail-closed", so a user reasonably assumes Telegram is the same.
Where
crates/ccteam-im/src/transport/providers/telegram.rs:43-51 — open_when_unset: bool, set to true for the global/owner bot at line 67; chat_allowed() returns it when allowed_chat_ids is empty (line 109-110).
crates/ccteam-im/src/daemon.rs:744-770 — bind_operator_rosters warns on OperatorBindingKind::Unconfigured but proceeds.
crates/ccteam-web/web/src/pages/SettingsPage.tsx around line 300-325 — the Telegram card has no open-mode warning.
The rationale in the source comment is "locking a half-configured owner out of their own box is the worse failure". I would argue the reverse: the owner has the console and can add a chat id there, while a stranger has the bot and needs nothing.
Suggested fix (any one of these closes it)
Fail closed, same as Lark. Empty allowlist answers nobody. Keep the existing RejectedSenderNotifier so an unbound owner gets the one-shot "not bound, add your chat id in the console" reply and can self-serve. This is the smallest diff: flip the default at telegram.rs:67 and drop the owner/tenant asymmetry.
Bind-on-first-DM with a pairing code. Console shows a short one-time code; the first sender who DMs that code gets bound; anyone else is rejected. Same UX the hint text already implies.
At minimum, surface the state in the UI: red badge "OPEN — any sender is operator" on the Telegram card while allowed_chat_ids is empty, plus the same line in ccteam doctor and ccteam status.
Happy to send a PR for option 1 plus the UI/doctor line from option 3.
How I found it
Fresh install, pasted a bot token to try the phone approval flow, read the daemon log while auditing egress. Mitigation on my side was to add my own chat id to ~/.ccteam/secrets/im-credentials.json before restarting the daemon.
Owner Telegram bot with an empty allowlist serves any sender as operator (open by default)
Version: ccteam 0.11.0 (3ed05d4), Linux x86_64, standalone install Severity: high in the common setup (fresh install, token pasted, no chat bound yet)
What happens
After pasting a @Botfather token in Settings › Access › Telegram and saving, the daemon starts polling that bot with no allowlist. In that state every Telegram user who finds the bot is treated as the operator: they can create sessions, send prompts, and (with the default bypassPermissions project settings, see the companion issue) run unattended shell and file operations on the daemon host.
The daemon knows this and logs it:
imd: the telegram bot has an EMPTY allowlist (open mode) — anyone who finds it is served as the operator; add your own chat id to close it
but nothing in the web UI says so. The Telegram card shows bound chats 0 and the hint "DM the bot to bind your chat", which reads as "the bot does nothing until you bind", i.e. the opposite of the real behavior. The Lark card next to it explicitly says "empty allowlist = fail-closed", so a user reasonably assumes Telegram is the same.
Where
crates/ccteam-im/src/transport/providers/telegram.rs:43-51 — open_when_unset: bool, set to true for the global/owner bot at line 67; chat_allowed() returns it when allowed_chat_ids is empty (line 109-110).
crates/ccteam-im/src/daemon.rs:744-770 — bind_operator_rosters warns on OperatorBindingKind::Unconfigured but proceeds.
crates/ccteam-web/web/src/pages/SettingsPage.tsx around line 300-325 — the Telegram card has no open-mode warning.
The rationale in the source comment is "locking a half-configured owner out of their own box is the worse failure". I would argue the reverse: the owner has the console and can add a chat id there, while a stranger has the bot and needs nothing.
Suggested fix (any one of these closes it)
Fail closed, same as Lark. Empty allowlist answers nobody. Keep the existing RejectedSenderNotifier so an unbound owner gets the one-shot "not bound, add your chat id in the console" reply and can self-serve. This is the smallest diff: flip the default at telegram.rs:67 and drop the owner/tenant asymmetry.
Bind-on-first-DM with a pairing code. Console shows a short one-time code; the first sender who DMs that code gets bound; anyone else is rejected. Same UX the hint text already implies.
At minimum, surface the state in the UI: red badge "OPEN — any sender is operator" on the Telegram card while allowed_chat_ids is empty, plus the same line in ccteam doctor and ccteam status.
Happy to send a PR for option 1 plus the UI/doctor line from option 3.
How I found it
Fresh install, pasted a bot token to try the phone approval flow, read the daemon log while auditing egress. Mitigation on my side was to add my own chat id to ~/.ccteam/secrets/im-credentials.json before restarting the daemon.