# 安全说明 · Security > dsh-lark-bot 把本机 DeepSeek Harness(`dsh`)暴露给飞书 / Lark IM。本文件说明威胁模型、 > 默认安全姿态与报告渠道。Security model for a bridge that exposes a local coding agent to Feishu / Lark. ## 官方分发渠道 · Official distribution channels - **唯一官方仓库**: - **唯一官方 npm 包**:`dsh-lark-bot`(同源双包 `dsh-feishu-bot`),维护者 `plutokeating` - **唯一安装命令**:`npx dsh-lark-bot@latest setup --profile dsh-lark` - **Releases 资产**:仅两个 npm tarball(`dsh-lark-bot-.tgz` / `dsh-feishu-bot-.tgz`), **从不发布 Windows/macOS 可执行文件** - **校验承诺**:自本文档更新后的下一个 Release 起,每个发布资产随附 `.sha256` 校验文件; 安装/使用前请核对 SHA-256,不一致即视为被篡改,请勿安装并报告 - **假冒识别**:任何以本项目名义提供“下载即运行”二进制(尤其 .exe)、或使用仿冒仓库名/包名的分发 渠道均为**假冒 / 恶意来源**——请勿下载或运行,并截图按下方报告渠道反馈 - **取证存档**:假冒仓库 `tarraencompassing61/dsh-lark-bot` 的取证与处置约定见 `docs/security/2026-08-17-impostor-repo-evidence/` - **持续监控**:`pnpm security:monitor`(假冒仓库活动 / npm 仿冒包 / 相似包名抢注),建议每周运行或挂 cron ## 威胁模型 · Threat model - **凭据泄露**:飞书 `app_id` / `app_secret`、DeepSeek API key、会话内容可能在日志、卡片或进程环境中出现。 - **越权访问**:未授权用户 / 群聊驱动本机 coding agent 执行命令、读写文件。 - **路径逃逸 / 符号链接逃逸**:附件、worktree、`/cd` 相关路径穿越到 bot 状态目录之外。 - **SSRF**:agent 或桥接层被诱导访问内网 / 环回地址。 - **消息重放 / 过期事件**:旧消息或重复事件被当作新指令处理。 - **交互工具不可达**:`ask_user_question`、终端类工具在 IM 场景下无法回达,应默认禁用。 - **救援通道被滥用**:dsh 下线后由守护接管飞书通道,若控制信号无鉴权,任何能私聊 bot 的人 都能触发安全模式或重启完整 profile。 ## 安全姿态 · Security posture 1. **默认拒绝**: - 群聊 / 话题普通消息必须 `@bot` 才响应;channel 以 `requireMention: false` 把事件交给 bridge,由 bridge 在匹配 pending 问答卡回复后执行 mention gate。只有精确回复问答卡可免 @。 - 配置了白名单后,私聊切换为 allowlist 模式(`dmMode: 'allowlist'`)。 - 可通过 `DSH_LARK_ACCESS_DEFAULT_DENY=1` 在无白名单时也拒绝私聊(默认关闭以兼容首次扫码绑定)。 2. **密钥脱敏**:结构化日志按字段名(`secret/token/password/api_key`)脱敏; 自由文本日志与卡片文本对 `Bearer …`、`sk-…`、`api_key=…` 做正则脱敏(`src/config/security.ts`)。 3. **路径 containment**:媒体下载目标、git worktree 目标必须落在各自根目录内 (realpath 校验,拒绝符号链接逃逸,`isPathWithin`)。 4. **UTF-8 安全截断**:附件文本、卡片摘要按字节截断且不切断多字节字符(`truncateUtf8Safe`)。 5. **过期事件拒绝**:消息时间戳超出窗口即拒绝(`isEventFresh`)。 6. **SSRF 防护**:仅允许 http(s) 公网地址;环回、私有、链路本地、CGNAT、IPv6 ULA 全部拒绝 (`isSafeHttpUrl`)。 7. **交互工具默认禁用**:SDK / ACP runtime profile 禁用 `user-questions`; `DEFAULT_DENIED_INTERACTIVE_TOOLS` 提供工具级黑名单。 8. **审批**:默认 SDK / Web 宿主在 `tools/pre-execute` 强制阻断高风险调用并通过 dsh rc.8 `approval/request` 回调,ACP 使用 `session/request_permission`。按隔离 scope 持久化的 `ask/allow/deny` 策略(0600,失败回滚且不报成功)决定弹一次性卡、自动放行或直接拒绝;只有管理员可修改, 显式目标仅限当前 chat 内 scope,`deny` 会在聊天中明确告知。该策略只作用于逐工具审批,不跳过后续独立的计划门禁;run 结束或 callback 断连只结算所属 session 的挂起请求。legacy headless 无工具回调,因此不宣称受该策略保护。 SDK / ACP / Web agent 对较大或高风险动作还会通过 `lark_request_plan_approval` 暂停;同一 turn 未批准时 pre-execute 策略拒绝写入、删除、移动、命令执行与 `run_code`。完整计划发到当前飞书 会话,只有卡片批准后才继续;继续规划会把可选文字意见返回 agent。run/callback 结束时只取消 所属 session 的挂起门禁。该门禁是人机确认层,不替代 dsh sandbox 或 ACP 的逐工具权限审批。 9. **管理操作鉴权**:飞书会话内对 dsh 配置与访问白名单的写操作(`/model default`、 `/model add|remove`、`/provider add|update|remove`、`/key set|remove`、`/invite user|admin|group` 与 `/invite remove …`)、`/permission ask|allow|deny`,以及群聊会话隔离模式写操作(`/isolation group|topic|member`)仅 profile 管理员可执行;首个扫码绑定的 operator 自动成为管理员,之后由现有 管理员经 `/invite admin ` 添加(`/invite list` 为只读、开放)。查看类命令 (`/model`、`/providers`、`/key list`)开放。`/doctor` 因包含本机运行状态与最近日志,仅管理员可执行。 `/replies set|default` 另允许当前群的群主/群管理员修改当前 scope;角色通过 `im.v1.chat.get` 的 `owner_id/user_manager_id_list` 以 `open_id` 实时校验,查询失败时拒绝写入。 10. **本地回调隔离**:`lark_notify`、`lark_send_file`、`lark_ask_user`、`lark_request_plan_approval` 与 `approval/request` answerer 的回调 服务只绑定 `127.0.0.1`,每次启动生成随机 token 鉴权(不落盘、不进日志),请求体限 1MB;`/notify` 与角色 / 配置写命令同为管理员操作。 文件回传不信任 runtime 自报 cwd:bridge 以 native session 反查 scope/workspace,只允许该 workspace、该 scope 实际 worktree/归档与实例日志内的 realpath 普通文件;以 no-follow 打开后 在同一文件句柄复核文件身份并有界读取,拒绝竞态 / symlink 越界、非法文件名和默认超过 20 MiB 的文件;agent 工具目标固定为该 session 的原 chat/thread。归档跨会话转发仅管理员可用。 主动通知偏好默认关闭;普通用户只能为当前 scope 设置当前目标,跨会话目标要求管理员且必须已在 `ScopeDirectory` 登记。偏好文件为 0600,提醒发送失败不回写或改变 durable job 终态。 回复合并与近似去重默认关闭;`reply-policies.json` 为 0600 且写失败回滚。近似去重只在同发送者、 同 immutable scope + workspace 与有限时间窗内生效,并对短文本要求规范化精确相等,降低误拦截 其他成员或不同项目任务的风险;命中时向原消息明确回执。 11. **多机器人 peer 鉴权与防循环**:只有 `fleet.json` 中已启用且 identity 唯一的 bot open_id, 在群内真实 @ 当前 bot 时才可交接;未知 bot、未 @、system/anonymous 消息拒绝。bot 文本不进入 slash-command 管理管线。连续交接由跨进程 `handoffs.json` 原子计数、按 messageId 去重,超过 `DSH_LARK_BOT_HANDOFF_MAX` fail closed;只有通过 freshness 检查的真人消息能重置计数。fleet、 handoff 与共享 `config.json` 写入均使用原子 owner 目录 + 唯一 token 子文件的 lease 锁并心跳续租; 回收/释放只删除精确 token,再对空目录 `rmdir`,不会误删替代 owner。dead-owner / 遗弃 lease 仍可回收。附加实例拒绝无法隔离广播 session 的共享 `web` adapter。 12. **安全网守护(默认随 `setup` 安装)**: - 守护是独立于 dsh / Cordis 的最小进程,只读取本地状态与进程命令行(`ps`,不读内存), 不导入任何 dsh 代码、不监听公网端口; - dsh 在线时守护**不连接飞书**(同 app 长连接仅允许单连接,避免抢占正常通道);仅在 「曾观察 dsh 在线 且 心跳过期 + 无 dsh 进程」时接管通道; - 控制信号默认拒绝:仅管理员(`access.admins`,无管理员时回退 `allowedUsers`)可触发 `/safemode` 系列命令,未授权消息静默丢弃; - 过期事件复用 `DSH_LARK_EVENT_FRESHNESS_MS` 窗口拒绝; - 心跳 / 守护状态文件以 `0600` 写入;安全模式仅挂载官方核心 bundle(headless: `dsh-base` + `dsh-headless`;SDK 流式优先:`dsh-base` + `dsh-sdk-jsonrpc-server`, 均不挂载第三方插件与 bridge 回调工具),避免把故障面带进救援通道。 ## 数据与凭据 · Data & credentials ### DSH session 投影与 TUI 信任边界 - Session 投影只在飞书用户显式确认后建立;WebUI/TUI 的 open、resume、switch 或 activity 不得 自动改变 binding,也不得把外部 session 广播给所有已知 scope。 - 选择器只枚举当前 canonical workspace 的非 subagent session 元数据,不展示正文。确认卡在历史 披露前列出标题/ID、workspace、更新时间、回填数量、scope、替换/迁移;取消、超时或 operator/ scope/workspace 不匹配时不绑定也不发送历史。 - 私聊遵循 allowlist;member 仅 scope owner;共享 group/topic 和跨 scope 独占迁移仅 profile 管理员。确认副作用在原子 store 事务中复核披露时 owner 与迁移授权;owner 变化必须重新开卡。 迁移同时清除旧 scope 的兼容 session mapping。一个 DSH session 默认最多绑定一个飞书 scope, 避免跨私聊/群聊的数据泄露。 - `session-projections.json` 为 0600 原子状态,只保存 routing/cursor/message mapping/rpcId,不复制 transcript。DSH append-only session log 是唯一真源,bridge 不直接修改 JSONL、不启动第二 writer。 - 来源标签只依据 DSH 事件提供的可信 provenance 或 bridge prompt correlation;无法确认时显示 “其他 DSH 客户端”,不根据进程、窗口或文本相似度猜测。飞书原始用户消息不被编辑。 - 根 `dsh-plugin.json` 的 host facet 运行于 `trusted-in-process`:它与 dsh-TUI 宿主共享进程权限, **不是安全沙箱**。可选 seam 缺失时 no-op,注册、定时器和状态均随插件 lifecycle 清理;不拦截 input/session switch,也不把 TUI observation/storage 当同步真源。 - 本地配置 `~/.dsh-lark/config.json` 以 `0600` 权限写入。 - 飞书凭据明文保存在本机配置文件;日志与卡片不输出真实密钥。 - dsh Web 设置页把 App Secret 声明为 Schemastery `role('secret')`:Host→browser 的 resolved/base/user 层均脱敏,只允许 write payload 单向进入官方 settings provider;设置卡不读取、预填或比较旧密钥。 Web 提交触发的 bridge reload 串行等待旧 generation 完整停止,避免新旧凭据实例同时连接。 - 卡片语言由飞书/Lark 客户端根据 Card JSON 2.0 的 `zh_cn` / `en_us` variant 本地选择;bridge 不读取、不推断也不持久化成员 locale。无法 per-viewer 选择的 Markdown/toast 直接并列中英文。 - 多机器人 registry `~/.dsh-lark/fleet.json` 只保存实例/profile 名与 bot open_id/name;共享 `handoffs.json` 保存 chat id、最近 message id 和轮数(均 0600)。这些标识会让本机用户看到 哪些机器人/群参与过交接;peer name/open_id 会进入每轮 agent prompt 并随任务上下文发送给 当前模型 provider,以支持精确 @ 交接。交接内容仍发送到共享群,不构成消息隐私隔离。 - 每个额外实例的 dsh provider 设置与凭据位于独立 `~/.dsh-lark/bots//dsh/{settings.yaml,.credentials.yaml}`;service env 快照标准 DeepSeek key 与该实例配置中已引用的 credential 环境键。`bot remove` 删除 `.credentials.yaml` 与 service env, 保留不含字面密钥的 settings/runtime session 以便恢复;其余 DSH_HOME 数据需由用户备份后手工清除。 - 桥接引擎始终在 dsh 宿主进程内运行。可选 `service install` 会将启动所需的 `DSH_LARK_*`、运行路径及实际 provider `credentialRef` 环境键白名单快照到 `~/.dsh-lark/service/.env`(POSIX 0600;Windows 用 `icacls` 移除继承并只授予当前用户);macOS plist / Windows 计划任务不嵌入密钥, 隐藏 runner 在启动时读取快照。敏感值不进日志与卡片;环境变更后需 `service restart` 刷新。 - 正常服务生命周期以 profile 级原子锁串行化;portable status 的 PID 必须同时匹配 Linux `/proc` starttime、`service-supervise` 命令和 profile 后才可发送信号,强制停止作用于已验证的 独立进程组,避免 PID 复用误杀或遗留孤儿 dsh。stop/uninstall intent 会阻止 guardian 回拉。 - 桥接引擎日志以 JSON Lines 输出到 stderr(由 dsh 宿主进程捕获),密钥字段脱敏后输出; - `/doctor` 诊断文件在内存生成并直接上传,不创建临时文件;仅包含非敏感配置计数、当前 workspace 的运行摘要、服务状态与最多 64 KiB 的当前 bridge 进程内结构化事件;不读取共享 dsh 宿主 stdout。 结构化事件只保留代码内固定枚举的 category/event 与固定数值字段, 时间被规范化,所有其他字段名和值均丢弃, 因而不含消息正文/transcript/凭据标识或值。 导出前会再次对 Bearer、`sk-`、`api_key`、当前进程已知敏感环境值及主目录脱敏。群中上传的文件 对群成员可见,因此命令仅限管理员,仍建议私聊生成并由发送者转发前复核。 `logs/bot.log` 是 0.6.0 独立服务时代的遗留路径,0.7.0 起不再写入。 - 聊天命令管理的 dsh 配置按官方存储协议写入:`~/.dsh/settings.yaml`(只存 `apiKeyEnv` 引用,不落字面密钥)与 `~/.dsh/.credentials.yaml`(目录 0700、文件 0600)。bot 永不回显 密钥值;群聊中粘贴密钥会对群成员可见,建议私聊使用或改用环境变量 / dsh Web 页面录入。 - 所有数据仅在本机、飞书开放平台与 DeepSeek API 之间流转;无遥测。 - 安全网守护相关文件:`~/.dsh-lark/guardian.json` 与 `~/.dsh-lark/profiles//guardian/heartbeat.json`(均 `0600`);守护读取的飞书凭据 来自 `~/.dsh-lark/config.json`(`0600`),日志按既有规则脱敏。 - 群聊隔离模式保存在 `~/.dsh-lark/profiles//isolation.json`(`0600`)。成员模式会把 飞书 `open_id` 作为 durable scope owner,因而该标识也会出现在对应 session、scope directory、 worktree 与 archive 的本地索引或路径中,并显示在共享群的运行卡片上。成员模式隔离的是 agent 上下文与会话数据,**不是群消息可见性**:任务输入、进度卡和回复仍发送到共享群,群内其他成员 仍可看到;其他成员不能操作该 member scope 的停止、审批或问答卡,缺失 operator identity 时也 拒绝操作。涉及私密内容时请改用私聊。 - adapter 实际上报的 input/output/cache token 与 context used/limit 保存在同一 profile 的 `sessions.json`(`0600`),并按 scope + canonical workspace cwd 隔离;最近 context 快照同时保存 产生它的 native sessionId 与 canonical provider/model 身份,并可由 `/status` 卡在身份匹配时展示。未知或身份不匹配字段不估算;member scope 的刷新动作 校验 operator `open_id` 与 owner,但共享群里已发送的状态卡仍对群成员可见。 - bridge 接收的普通 agent 消息在入队前写入 `profiles//jobs.json`(0600):包含原始正文、 附件/提及元数据、chat/thread/scope、workspace、状态及受控 checkpoint,最多保留 500 条终态记录。 checkpoint 不含隐藏推理正文或工具参数;`/jobs` 展示先脱敏并按 scope + workspace 隔离。原始 prompt 仍可能包含用户主动输入的密钥,安全边界与 `sessions.json` 相同。running 崩溃后只标记 interrupted, 不自动重跑可能已有副作用的工具;显式 retry 会再次执行,用户必须先对账。 - 审批卡会把工具名、理由、调用标识及可取得的执行参数发送到当前会话;member scope 只限制谁能 点击,并不隐藏卡片正文。涉及密钥、私有路径或敏感命令时应使用私聊。 - 群消息在底层 channel 进入 bridge 后执行 mention gate:普通群消息仍需 @bot(或管理员明确开启 no-at 模式);仅当 `replyToMessageId` 命中当前进程内 pending 问答卡时可免 @。文字答案必须属于 同 chat/topic,member scope 还要求 sender `open_id` 等于 owner;拒绝的回复不会结算问题或进入任务队列。 no-at 的实时事件与历史轮询都再次校验当前 `allowedUsers` / `allowedChats`。`scopes.json` 会保存 每个 scope 最近一次入站 messageId,作为 topic 问答卡的 reply anchor。 ## 报告渠道 · Reporting 发现安全漏洞请通过 GitHub Security Advisory 私下报告,**不要**公开 issue。