Cherry Studio 渠道入口安全设计:无监督 Agent 外部触发运行(Channel Ingress Security)的威胁模型、现有防线与待修复缺口
2026/9/20 13:48:07 网站建设 项目流程
  • 人工智能
  • 大模型
  • AI 应用
  • 交互助手
  • 本地部署

【免费下载链接】cherry-studio

🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端

项目地址:https://gitcode.com/CherryHQ/cherry-studio
点击查看免费下载

外部即时通讯(IM)消息驱动的 Agent 运行,与桌面端用户在渲染器(renderer)中发起的交互式运行存在本质差异:没有人在看屏幕、没有交互式审批界面、入站内容完全来自不受信任的远端。本文基于 Cherry Studio 仓库中的设计文档 channel-ingress-security.md 展开,结合 ChannelMessageHandler.ts、OutputSanitizer.ts、settingsBuilder.ts 等源码实现,系统梳理入站链路(ingress flow)、当前已具备的安全防线、四个待关闭的安全缺口(G0–G3)以及推荐的安全基线姿势。读完本文,你将掌握:渠道运行与桌面运行在信任边界上的差异、现有防御层的具体代码落点、每个缺口的攻击含义与修复方向,以及如何在渠道场景下配置 Agent 使其达到保守安全基线。

威胁模型:一个不受信任的远端发言者驱动一个能触碰工作区的 Agent

渠道入口安全要回答的核心问题是:当一条来自不受信任远端方的消息,经由绑定的渠道(Slack / Discord / Telegram / 飞书 / 微信 / QQ)进入系统,驱动一个可以调用工具、读写会话工作区的 Agent 时,如何保证运行安全?这条运行链路没有人类在渲染器侧监督,因此它不能依赖任何"人工确认"机制作为兜底。

设计文档明确给出了这条链路的完整拓扑:

adapter(webhook/socket)→ ChannelManager 注册 adapter.on('message', …) → ChannelMessageHandler.handleIncoming(按会话 1s 防抖 + 串行队列) → processIncoming 解析绑定的 session + agent → startAgentSessionRun({ sessionId, userParts, listeners })

其中每一环在源码中都有确切的落点,可以一一对应验证:

  1. adapter 事件注册ChannelManager.ts中通过adapter.on('message', (msg) => …)建立入站消息监听(ChannelManager.ts),收到消息后调用channelMessageHandler.handleIncoming(adapter, msg)
  2. 入站入口与串行化handleIncoming(ChannelMessageHandler.ts)以${agentId}:${channelId}:${conversationId}:${userId}为键做每会话防抖批处理:默认MESSAGE_BATCH_DELAY_MS = 1000(1 秒),单个发送者最多可延长到MESSAGE_BATCH_MAX_DELAY_MS = 16000(16 秒),避免微信等 IM 用户连发短消息时每条都触发一次 Agent 往返;随后通过enqueueBatch进入每会话串行队列chatQueues),保证同一会话同一时刻只有一个流在运行,杜绝并发交错(ChannelMessageHandler.ts)。
  3. 会话与 Agent 解析processIncoming先解析该会话绑定的 Agent 与工作区,若命中孤儿会话(agentId === null)则拒绝运行;随后把入站图片/文件持久化到工作区,并将文本与附件路径拼接后交给collectStreamResponse(ChannelMessageHandler.ts)。
  4. 单次运行约束:最终调用startAgentSessionRun({ sessionId, userParts, listeners, headless: true, requireIdle: { expectedAgentId } }),并通过requireIdle保证同一${agentId}:${channelId}:${conversationId}同时只有一个运行(ChannelMessageHandler.ts)。

设计文档中提到的handleIncoming:54:111)与ChannelManager.ts:301为文档写作时的近似行号,对应现代码中防抖/队列逻辑与adapter.on('message')注册位置。

已就位的防御层:五道防线及其源码落点

设计文档将"已经存在"的防御整理为一张表。结合源码,每一层都能看到具体实现:

防御层代码位置实际作用
输出密钥脱敏OutputSanitizer.ts(sanitizeChannelOutput,在 ChannelMessageHandler.ts 附近被调用)在 Agent 输出离开渠道之前,将 PEM 私钥、AWS / GitHub / Anthropic / OpenAI 密钥、Bearer Token 等替换为[REDACTED]
工作区隔离会话workspace.path;附件持久化在${workspace}/.cherry-studio/channel-*Agent 的 fs 访问范围被限定在会话工作区内,但其强度取决于 Agent 的工具策略:一个绑定了宽泛 Bash/Write 且未做每渠道收窄的 Agent,实际上不会被有效约束
渠道白名单各平台 adapter 的allowed_chat_ids/allowed_channel_ids配置(见下文)来自未白名单会话/频道的入站消息被静默丢弃
每会话串行化ChannelMessageHandler.ts 的chatQueues同一会话同一时刻只有一个流,杜绝并发交错
写静默(write-quiesce)入口闸门ChannelMessageHandler.ts 的isWriteQuiesced检查备份恢复期间暂停渠道摄入,缓冲批次立即冲刷,静默期到达的消息被丢弃并告警(与 JobManager / AiStreamManager 的编排契约)

输出脱敏的具体匹配模式

sanitizeChannelOutput(OutputSanitizer.ts)首先剥离[cite:...]内部引用标记(保留代码块中的字面示例),随后依次执行 9 组正则模式替换:

  • PEM 私钥-----BEGIN (RSA|EC|OPENSSH) PRIVATE KEY----- … -----END … PRIVATE KEY-----
  • AWS 访问密钥 IDAKIA+ 16 位大写字母数字
  • AWS 秘密访问密钥aws_secret_access_key =之后的 40 位 base64
  • Bearer TokenBearer+ 20 字符以上
  • 通用 key=value 密钥api_key/password/token/client_secret等键名后 16 位以上值
  • GitHub PATghp_前缀 36 位以上
  • Anthropic 密钥sk-ant-前缀
  • OpenAI 密钥sk-sk-proj-前缀
  • SSH 公钥内容AAAA开头的 100 位以上 base64(要求混合大小写与数字,避免误伤统一字符串)

每次命中都会记录logger.warn('Redacted sensitive content from channel output', { pattern: name }),并返回{ text, redacted }供上层判断是否发生了脱敏。

渠道白名单的跨平台实现

允许列表按平台命名略有差异,但语义一致:列表为空 = 不限制;列表非空 = 仅允许列表内的会话/频道

  • 飞书allowed_chat_ids(FeishuAdapter.ts),入站时if (this.allowedChatIds.length > 0 && !this.allowedChatIds.includes(message.chatId))静默丢弃;
  • Discordallowed_channel_ids(DiscordAdapter.ts),同时匹配原始频道 ID;
  • QQallowed_chat_ids,支持群 ID 白名单(QqAdapter.ts),mention_only=false时仍会尊重白名单过滤;
  • Slackallowed_channel_ids,且会把白名单同步进notifyChatIds(见 SlackAdapter.test.ts 的测试);
  • Telegram / 微信allowed_chat_ids,微信 adapter 的测试明确覆盖了"白名单外用户被过滤"与"白名单内用户被放行"两种场景(WeChatAdapter.test.ts)。

此外,/whoami命令会直接向当前会话回复Current chat ID: …,并提示"把这个值加入allowed_chat_ids(Discord 为allowed_channel_ids)即可接收通知"——这是白名单配置的常用获取方式(ChannelMessageHandler.ts)。

信任边界总结

设计文档用一句话概括了当前边界:

入站文本原样传递;入站文件/图片不做内容检查(持久化到工作区,Agent 经 Read 工具读取,受工作区约束);出站做密钥脱敏;发送者身份未校验(见缺口 1)。

待关闭的缺口:D1 评审的实质工作

设计文档指出当前实现与 v1 保持平级(v1 也没有入站认证),以下四个缺口是渠道入口安全后续工作的主体,按 G0–G3 编号。

G0 — 入站内容没有来源边界

入站文本被刻意原样传递给 Agent:不携带发送者前缀、边界标记、注入警告、归一化或检测日志。这是产品层面的显式决策——提示词包装(prompt wrappers)被移除,Agent 收到的是用户消息的字面内容。

安全含义:任何渠道内容都应视为不受信任输入。配置渠道绑定 Agent 的工具与权限时,必须以"这条文本可能来自恶意攻击者、可能包含提示注入"为前提。

G1 — 授权是会话级而非发送者级

各 adapter 的闸门作用于会话/频道白名单,因此:白名单群聊中的任意成员都能触发 Agent 运行

修复方向(设计文档建议):可选的每渠道发送者白名单(用户 ID 列表),在 adapter 中与会话检查并列执行;默认关闭(会话级检查仍是基线),群聊场景可选开启;拒绝时静默丢弃(与会话闸门行为一致)。注意:现有的消息批处理键已包含message.userId(ChannelMessageHandler.ts),这为发送者级过滤提供了现成的数据基础。

G2 — 无监督运行的工具有审批但没有应答方

渠道运行不绑定任何渲染器,因此审批的emit无绑定对象。在 settingsBuilder.ts 的canUseTool回调中可以看到完整的拒绝链:

  1. 会话无实时工具策略快照 →'Tool policy not ready'(拒绝);
  2. 工具需要交互式应答(如 AskUserQuestion)且interactionState.userResponse === 'unavailable'→ 按无应答者拒绝;
  3. hasLiveTurnStream === false(无实时交互流,渠道运行即属此类)且非后台 Agent →'Approval requested outside a live interactive turn — denying'(拒绝);
  4. 审批 emitter 未绑定(渠道运行下peekToolApprovalEmitter返回空)→'Approval emitter not ready'(拒绝)

净效果:需要审批的工具在渠道运行中直接失败,除非 Agent 被设置为bypassPermissions——而这恰恰是不安全的绕过方式。这是外部运行设计的关键漏洞(设计文档原文:"This is the key external-run design hole")。

设计文档给出了两条可选方案(按产品意图二选一并记录决策):

  • 策略驱动、无交互卡片(推荐):渠道运行下,把每个工具解析为allow/deny非交互式策略(Agent 的permission_mode+ 每渠道工具允许/拒绝列表),永不"询问"。未列入白名单的需审批工具以清晰、模型可见的理由拒绝(如 "not permitted on this channel"),让 Agent 可以继续或解释,而不是挂起。
  • 带外审批(Out-of-band):通过渠道本身(回复 approve/deny)或配套渲染器通知把审批呈现给人类。更重,仅在"渠道上交互式审批确实是硬需求"时才采用。

G3 — 缺少每渠道权限覆盖

v1 允许渠道覆盖 Agent 的permission_mode;v2 把配置迁移到 Agent 身上后丢弃了该能力。源码中的两处 TODO 佐证了这一状态:

  • ChannelMessageHandler.ts:TODO(channel-perm-override): channel-level permission_mode used to mutate session.configuration in-place; with config now living on agent, this override needs to flow as a per-dispatch option instead. Tracked separately.
  • ChannelMessageHandler.ts:/new命令路径上的同类 TODO。

后果:渠道无法比其绑定的 Agent更严格(例如:为一个权限宽泛的 Agent 提供只读工具集)。

修复方向:每渠道permission_mode+ 工具允许/拒绝覆盖,作为每分派选项(per-dispatch option)线程化传入startAgentSessionRun→ Claude Code 的toolPolicySnapshot,叠加在 Agent 策略之上;渠道只能收窄(narrow),绝不能放宽(widen)。这也是 G2 策略驱动方案所读取的杠杆。

推荐的保守基线(在 G1–G3 落地之前)

设计文档明确:渠道是opt-in、高信任的特性。在缺口关闭前,应记录并遵循以下保守默认值:

  1. 只对可信工作区启用渠道——渠道不是默认开启的能力;
  2. 要求显式会话白名单——每个渠道必须配置allowed_chat_ids/allowed_channel_ids
  3. 给渠道绑定的 Agent 配置只读工具集——避免宽泛 Bash/Write 工具在没有人工监督时被远程触发;
  4. 不要对渠道连接的 Agent 使用bypassPermissions——这是当前唯一"让工具在渠道上跑起来"的办法,但也是最不安全的办法。

修复优先级:G2 是第一个要修复的缺口,因为当前唯一让工具在渠道上工作的答案(bypassPermissions)恰是最不安全的。

状态与边界说明

设计文档声明该方案"在本 PR 中未实现——与 v1 平级(v1 也没有入站认证)",作为后续工作跟踪,本文档即评审者(D1)要求的设计答复。因此,本文中所有"建议方向""方案选项"均属于设计意图而非已落地功能;所有"已就位防御"均有源码与测试佐证。在 G1–G3 落地前,渠道运行的安全强度完全取决于管理员是否遵循上述保守基线。

相关代码与文档入口:

  • 设计文档:v2-refactor-temp/docs/ai/channel-ingress-security.md
  • 入站处理核心:ChannelMessageHandler.ts
  • 渠道管理:ChannelManager.ts
  • 输出脱敏:OutputSanitizer.ts
  • 工具审批回调:settingsBuilder.ts
  • 各平台白名单实现:FeishuAdapter.ts、DiscordAdapter.ts、QqAdapter.ts、SlackAdapter.ts、TelegramAdapter.ts、WeChatAdapter.ts
  • 人工智能
  • 大模型
  • AI 应用
  • 交互助手
  • 本地部署

【免费下载链接】cherry-studio

🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端

项目地址:https://gitcode.com/CherryHQ/cherry-studio
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询