fix(chat): require per-channel sender allowlist for inbound commands#309
fix(chat): require per-channel sender allowlist for inbound commands#309jtalborough wants to merge 1 commit into
Conversation
) * fix(chat): require per-channel sender allowlist for inbound commands Chat-channel commands (/task, /folder, /approve, …) spawn and drive agents with the host's full privileges, but the dispatcher routed purely on the command verb — sender_id was logged and used for context yet never authorized. Any user who could message the bot could run arbitrary tasks (RCE) and /approve always disabled the human-in-the-loop. This is acute for an always-on server reachable via Telegram. Add a fail-closed authorization gate at the top of dispatch_command (covers Telegram, Lark, and WeChat in one place): only sender ids listed in the channel's config_json `allowed_senders` may drive the bot. An empty/unset list authorizes no one; a blocked sender is told their own id so the operator can add it. Surfaced in the add/edit channel dialogs as an "Allowed Sender IDs" field. - chat_channel/authz.rs: pure is_sender_allowed() + unit tests - command_dispatcher.rs: gate before any command, incl. follow-ups - i18n.rs: unauthorized reply (10 languages) - add/edit-chat-channel-dialog.tsx: allowlist management UI Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(chat): annotate allowedSenders types for strict tsc `pnpm build` (strict tsc) flagged the `.map((s) => …)` callback as implicit `any`: in the edit dialog `config` comes from `JSON.parse` (any), so the `allowedSenders` state inferred `any`. Type the state as `string` and annotate the map parameter. eslint doesn't run the type-checker so it passed locally; validated now with `pnpm build` + `pnpm test` (1747 passing). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
First off — thank you for this. 🙏 Reporting a real authorization gap with the fix attached (plus unit tests and fully localized replies) is exactly the kind of contribution we love to get. The design is solid: fail-closed by default, a single gate in I went through it carefully (and had a second reviewer double-check the security-sensitive parts). Here's where it stands — a few things to sort out before we merge, mostly because 1. Rebase needed — and it's security-relevant, not just textual
if let Some(data) = callback_data {
return DispatchResponse::current(handle_callback(...), target);
}Your gate is anchored just before the Severity here is genuinely low — 2. One functional gap worth fixing before this ships for WeChatThe WeChat QR flow ( Could the QR path merge Non-blocking — take or leave
None of this takes away from the core, which is exactly the protection we want. Happy to help with the rebase or the WeChat config-merge fix if that's easier on your end — just say the word. Thanks again for flagging this and for doing it so thoroughly. 🙌 |
Summary
Adds a fail-closed, per-channel sender allowlist so only authorized senders can drive agents through chat channels (Telegram / Lark / WeChat).
Why (security)
dispatch_commandcurrently routes purely on the command verb — the inboundsender_idis logged and used for per-sender context, but it is never authorized. The chat commands it routes to are not read-only:/taskspawns and drives an agent with the host's full tool access, and/approve alwaysenables auto-approval that removes the human-in-the-loop for subsequent tool calls.The practical consequence: anyone who can message the bot (e.g. anyone who finds a Telegram bot's handle, or is in a group it's added to) can run arbitrary tasks → effectively unauthenticated remote code execution on the host, especially for an always-on / internet-reachable deployment.
chat_idonly scopes outbound messages; inboundgetUpdatesdelivers from any chat.What this does
dispatch_command(one place → covers all backends). Only sender ids listed in the channel'sconfig_json.allowed_sendersmay drive the bot.chat_channel/authz.rs— pureis_sender_allowed()with unit tests.Validation
cargo test/cargo clippy -D warnings(server mode) andeslintpass.Notes
i18n/messages/*if you'd prefer that before merge.🤖 Generated with Claude Code