Skip to content

bug(security/channels): channel_send account_id accepted unvalidated — cross-tenant dispatch across bot accounts #6443

Description

@nevgenov

Summary

channel_send's account_id parameter is accepted from the model with zero validation — an agent can dispatch a message to any configured channel account (i.e. any other bot/tenant instance registered on the daemon), not just the one it was invoked from. In a multi-tenant setup (distinct bot accounts serving different customers/organizations on the same daemon), this is a cross-tenant confidentiality boundary, not just a UX footgun.

This is the same bug class as #6117 (cross-chat dispatch via a mismatched recipient on the same channel) but a wider blast radius: #6117's fix explicitly scopes itself to same-channel re-targeting and states cross-channel dispatch "stays allowed" by design. account_id (cross-account, i.e. effectively cross-tenant) sits entirely outside that guard.

Verified against the current stable release v2026.7.11 (pinned to the git tag, not main).

Root cause

crates/librefang-runtime/src/tool_runner/channel.rs, inside tool_channel_send():

let thread_id = trim_opt_string(input["thread_id"].as_str());
let account_id = trim_opt_string(input["account_id"].as_str());

account_id is extracted from the tool-call input and passed straight through to kh.send_channel_media(...) / the text-send equivalent with no allowlist check against the calling agent, no comparison to the turn's originating channel/account, and no capability gate beyond the coarse channel.* capability that merely grants/denies the tool as a whole.

The #6117 cross-chat guard (same file, a few lines above) only fires when:

if let (Some(explicit), Some(turn_channel)) = (explicit_recipient, sender_channel) {
    ...
    if turn_channel.eq_ignore_ascii_case(&channel) && explicit != expected { ... }
}

— i.e. it compares channel (the channel identity the request is being sent on, e.g. a specific bot instance) against the turn's inbound channel, and only rejects a mismatched recipient within the same channel. A channel_send call that targets an entirely different channel/account_id bypasses this check completely — which the issue that introduced it (#6117) explicitly calls out as intentional scope-narrowing ("a different-channel dispatch... stays allowed").

Related but orthogonal existing mechanism: ChannelToolRule (crates/librefang-types/src/approval.rs) gates which tools may run for requests originating from a channel (inbound). It has no notion of restricting which channel/account an agent's outbound channel_send may target.

Why this matters (multi-tenant impact)

A common deployment shape is one daemon running several bot instances, each serving a different customer/organization (e.g. bot-A for customer A, bot-B for customer B), each bound to its own agent via default_agent. If the agent behind bot-A can be induced — via model hallucination or prompt injection in an inbound message it processes (the same trigger class documented as the root cause in #6117's live incident) — to call channel_send(account_id="bot-B", ...), it will silently dispatch attacker- or model-chosen content into a different customer's chat. No approval, no warning, no audit distinction from a same-account send.

Proposed fix

Minimal: extend the existing #6117-style guard to also validate account_id (when present) against the turn's originating account, using the same "explicit-value-only" scoping (auto-filled sends already target the correct account).

More complete: an opt-in per-agent capability, e.g.:

[capabilities.channel_send]
allowed_accounts = ["bot-A"]
allowed_peers = ["<peer-id>"]

enforced the same way ChannelToolRule enforces inbound tool gating today, so operators running single-tenant setups aren't forced to enumerate every legitimate escalation path.

Workaround in use today: replace the native channel_send in the agent's capabilities.tools with a custom MCP tool whose schema hardcodes account_id/peer_id as constants (the agent physically cannot pass a different value — the parameter isn't in the schema).

Setup (for reference)

Happy to test a patch.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/channelsMessaging channel adaptersarea/securitySecurity systems and auditinghas-prA pull request has been linked to this issue

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions