Skip to content

插件授权事实源应与安装目录解耦:宿主需保存 approved manifest #636

Description

@nanaco666

来源标注 / Provenance

内容
来源渠道 私密邮件反馈 → feedback@cindy.app
报告者 岳巍(外部报告者)
报告日期 2026-07-25 (Asia/Shanghai)
本 issue 由 维护者代为转录并补充代码定位
披露范围 ⚠️ 本 issue 为删节版。按 SECURITY.md,复现步骤、可利用路径与相关调用链不在公开渠道披露,仅在私密渠道与报告者协调。此处只保留架构缺口描述与加固需求。

一、报告者原文(节选转录)

我想私密反馈一个与 Cindy Desktop 本地插件更新流程有关的潜在安全问题。

当 Forge/Agent 直接使用某个已安装插件目录制作新版本时,源码修改会在用户确认更新之前直接改变安装目录中的 ghost.json。后续更新流程可能无法取得最近一次经过用户批准的旧 manifest,从而导致更新确认弹窗中的版本变化显示错误。

初步代码检查还发现,权限差异和部分运行时授权判断也可能依赖安装目录中的当前 manifest,而不是宿主独立保存的已批准状态。因此,该问题可能不只是界面显示错误,还可能涉及插件权限确认和运行时授权边界。

关于流程代表性,报告者的说明(原文):

项目文档建议插件源码位于用户 workdir 中的独立目录。因此,直接在已安装插件目录中制作更新不是推荐的插件开发流程。

不过,目前产品流程中存在以下情况:

  1. 插件详情界面能够显示、复制或打开插件安装目录。
  2. 用户或 Agent 可以自然地进入该目录继续修改插件。
  3. Forge/Agent 当前没有阻止已安装插件目录被用作源码目录。
  4. 更新流程也没有检测当前安装状态是否已在确认前发生改变。

因此,我认为该问题不能完全归因于用户未遵守推荐流程。即使正常流程建议使用独立 workdir,宿主仍应保护安装目录、已批准 manifest 和运行时授权边界,避免自然可达的操作路径导致确认信息失真。

报告者的预期结果(原文):

  1. 更新弹窗中的旧版本必须来自最近一次经过用户确认的安装状态。
  2. 旧权限和旧能力声明必须来自宿主保存的 approved manifest,而不是安装目录中的当前 ghost.json
  3. 候选插件的 manifest 应与当前已批准 manifest 独立保存和比较。
  4. 用户确认更新之前,对候选插件的修改不应改变当前插件的运行时授权状态。
  5. 如果检查完成后当前安装状态发生变化,原更新确认应失效,并要求用户重新检查和确认。
  6. Forge/Agent 不应允许把已安装插件目录或其子目录直接作为插件源码目录。

二、架构缺口(维护者确认,main @ 39afc9b

报告者的判断成立。核心是一句现有设计注释(apps/desktop/src/main/cindy-brain/GhostManager.ts:86):

目录即注册表:没有额外的 DB / 索引文件,list() 每次实扫磁盘

即:宿主不保存任何独立的、经用户批准的 manifest 副本。插件安装目录中的 ghost.json 是唯一事实源,而它是可变的、不在宿主保护范围内的普通文件。

这一个根因派生出三处依赖:

  1. 更新弹窗的"旧版本" —— apps/desktop/src/renderer/cindy-brain/installFlow.tsx:102from 取自 installed.manifest.versioninstalled 来自现读安装目录。
  2. 更新弹窗的权限 diff 基准 —— installFlow.tsx:98diffGhostPermissionItems(installed.manifest, manifest) 的左值是同一份现读结果。
  3. 运行时能力判断 —— 读取 manifest.slots / manifest.network 等声明的路径同样经 GhostManager.list() 取得,未与已批准状态解耦。

此外 apps/desktop/src/main/cindy-brain/forge.ts:528packGhostDir(dir) 只接受目录参数,无路径归属校验;而同文件 :394scaffoldGhostDir 已有完整的 realpath + 会话 workdir 收敛校验。两者护栏不对称。

同样地,ghosts:updateapps/desktop/src/main/cindy-brain/index.ts:3144)已用 expectedPackageSha256 防止候选包在用户确认后被替换:

if (inspected.packageSha256 !== expectedPackageSha256) {
  throwIpcError('GHOST_FILE_INVALID', '插件文件在确认后发生了变化,请重新选择并确认');
}

缺失的是对称的另一侧 —— 已装侧没有等价的 revision 校验


三、加固需求

  1. 安装时由宿主落一份 approved manifest(含安装 receipt 与 revision),存放位置不在插件目录内、不可由插件源码流程直接改写。
  2. 更新弹窗的旧版本与权限差异一律以 approved manifest 为基准
  3. 运行时能力/授权判断改用 approved manifest,不直接信任可变的 live ghost.json
  4. packGhostDir 增加安装目录及其子目录拒收,基于 realpath,覆盖软链接、目录联接与 Windows 路径大小写等绕过方式。
  5. ghosts:inspect / ghosts:update 携带并校验 expected installed revision;不一致即作废并要求用户重新确认。
  6. 插件信任元数据(.cindy-trust.json)同样纳入宿主侧保存或加签校验。
  7. 修改已有插件时,Forge/Agent 先将插件复制到会话 workdir 形成独立工作副本。
  8. 补回归测试:版本差异、权限差异、状态竞争、软链接、旧安装兼容、Windows/macOS 路径。
  9. 同步更新 Forge 编写手册与相关工具契约,明确安装目录不能作为源码目录。

四、环境

  • 报告者复现环境:Cindy Desktop 0.1.10,Windows 10 Pro 22H2 (Build 19045.6456, x64)
  • 维护者代码定位:main @ 39afc9b(macOS)
  • 复现载体为报告者本地自制、未对外分发的插件,不含真实用户数据、令牌、凭证或内部服务地址

五、致谢

感谢 岳巍 通过 feedback@cindy.app 私密报告本问题,并在维护者完成影响确认前主动遵守协调披露约定。

复现细节、完整调用链与影响评估在私密渠道继续跟进;本 issue 仅用于公开跟踪上述加固项的落地。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions