Skip to content

插件系统:增加安全的 ZIP 导入与原子安装 - #48

Merged
Playa-0v0 merged 4 commits into
Playa-0v0:masterfrom
liyi3068238601-oss:feat/plugin-zip-import
Sep 1, 2026
Merged

插件系统:增加安全的 ZIP 导入与原子安装#48
Playa-0v0 merged 4 commits into
Playa-0v0:masterfrom
liyi3068238601-oss:feat/plugin-zip-import

Conversation

@liyi3068238601-oss

Copy link
Copy Markdown
Contributor

PR 关系与审查范围

本 PR 实现上游在插件系统审查中提出的社群分发流程:

导入 ZIP → 安全解压到用户插件目录 → 自动重扫 → 用户确认后再启用

本 PR 依赖 #42(插件运行时、生命周期、资源所有权与卸载)。当前以 #42 的头提交为父提交,新增功能集中在提交 5684b908。在 #42 合并前,GitHub 的整体 diff 会同时显示基础插件系统;建议本轮优先审查该新增提交。#42 合并后会将本分支 rebase 到最新 master,届时该 PR 只保留 ZIP 导入增量,再转为 Ready for review。

用户体验

设置页“功能插件”区域新增“导入 ZIP”按钮:

  1. 用户点击“导入 ZIP”;
  2. Electron 原生文件选择器只展示 .zip
  3. 主进程在隔离的 staging 目录中检查并解压;
  4. 新插件安装到 %APPDATA%\live2d-cyrene\plugins\<plugin-id>\
  5. 安装成功后自动重扫并显示在插件列表中;
  6. 新导入插件保持停用,用户确认来源可信后才能手动启用;
  7. 如果同 ID 用户插件已经存在,显示原生替换确认框;取消不会改变现有插件;
  8. 替换只更新插件程序,继续保留 plugin-data/<plugin-id>/ 私有数据。

ZIP 支持两种常见结构:

# 结构一:文件直接位于 ZIP 根目录
manifest.json
index.cjs

# 结构二:唯一顶层目录
my-plugin/
├── manifest.json
└── index.cjs

正式安装目录始终由经过校验的 manifest.id 决定,不信任压缩包文件名或顶层文件夹名。

安装事务

第一阶段:选择文件

  • 新增 plugins:import-zip IPC;
  • Preload 只暴露无参数的 window.plugins.importZip()
  • Renderer 不能向 Main 任意传入文件系统路径;
  • 实际路径由 Main Process 的 Electron 原生对话框取得。

第二阶段:隔离解压和预检

ZIP 不会直接解压到 userData/plugins。每次导入在以下目录创建随机 staging:

userData/plugin-install-staging/<random-uuid>/

检查或解压失败时递归清理该次 staging,不触碰正式插件目录。

第三阶段:插件结构校验

解压完成后复用插件 loader 的正式校验逻辑:

  • apiVersion 必须为当前 v1;
  • ID 必须符合小写连字符格式;
  • version 必须是合法 SemVer;
  • deps 必须属于宿主支持的能力;
  • entry 必须是插件目录内的普通 .cjs.js.mjs 文件;
  • entry 真实路径不能通过链接越出插件目录。

这避免“扫描时一套规则、安装时另一套规则”造成差异。

第四阶段:冲突确认与原子提交

  • 内置插件 ID 永远不能被用户 ZIP 覆盖;
  • 已有用户插件必须得到明确替换确认;
  • 替换时先将旧目录同卷 rename 到独立备份目录;
  • 再将 staging 中已校验的插件目录 rename 为正式目录;
  • 新版本提交失败且正式目录不存在时,恢复旧目录;
  • 新版本提交成功后才清理旧版本备份;
  • 整个 install 操作与 enable、disable、rescan、uninstall、stop 共用插件生命周期串行队列。

第五阶段:状态和重扫

  • 全新插件会清除可能残留的同 ID 启用记录;
  • 因此即使 ZIP 的 defaultEnabled 为 true,首次导入仍保持停用;
  • 替换已启用的现有用户插件时保留用户既有启用选择,提交成功后通过正常重扫重新加载;
  • 安装成功自动重扫,无需重启应用;
  • userData/plugin-data/<plugin-id>/ 不参与程序目录替换。

ZIP 安全边界

路径与文件类型

导入会拒绝:

  • 绝对路径、盘符路径和 UNC 形式;
  • ...、NUL 和 NTFS ADS 冒号路径;
  • Unix ZIP 符号链接;
  • 解压后发现的符号链接或非普通文件;
  • Windows 保留名称,如 CONNULCOM1LPT1
  • 尾随空格或句点的 Windows 不稳定路径;
  • 重复路径和仅大小写不同的冲突路径。

extract-zip 自身的目标目录边界检查仍然保留,本 PR 的检查属于额外的 fail-closed 层。

资源上限

  • ZIP 文件:最多 50 MiB;
  • 条目数量:最多 2000;
  • 单文件解压后:最多 50 MiB;
  • 总解压量:最多 200 MiB;
  • 超过 200:1 的异常单条目压缩比拒绝;
  • 加密 ZIP 拒绝。

这些限制用于降低 ZIP bomb、资源耗尽和平台路径歧义风险。它们不代表插件代码进入安全沙箱;插件仍是 Electron Main Process 中的可信本地代码。

主要文件

  • src/plugins/installer.ts:ZIP 预检、staging、结构校验、备份与原子提交;
  • src/plugins/installer.test.ts:自包含 ZIP fixture 与安全回归;
  • src/plugins/manager.ts:安装生命周期、冲突确认、状态处理和重扫;
  • src/plugins/loader.ts:导出统一 manifest 检查入口供扫描和安装复用;
  • src/main/plugin-runtime.ts:原生选择文件和替换确认对话框;
  • src/shared/ipc-channels.tssrc/preload/index.ts:最小导入桥接;
  • src/renderer/settings/feature-plugins/**:导入按钮、忙碌状态和错误展示;
  • docs/plugins/plugin-authoring.md:ZIP 结构、事务、安全上限和验证说明。

自动化测试

ZIP 与插件专项测试

以下 4 个测试文件共 44/44 通过:

  • src/plugins/installer.test.ts
  • src/plugins/loader.test.ts
  • src/plugins/manager.test.ts
  • src/renderer/settings/feature-plugins/panel.test.ts

新增覆盖包括:

  • ZIP 根目录结构;
  • 唯一顶层目录结构;
  • 最终目录使用 manifest ID;
  • Zip Slip 路径穿越拒绝且不产生越界文件;
  • Unix 符号链接拒绝;
  • Windows 大小写路径冲突拒绝;
  • 声明异常解压尺寸拒绝;
  • 无效 manifest 在接触正式目录前拒绝;
  • 替换旧程序且保留程序目录外的插件数据;
  • 导入 IPC、自动重扫和首次停用;
  • 设置页导入交互与列表刷新。

完整构建

npm run build 通过:

  • Main TypeScript;
  • Preload TypeScript;
  • CLI bundle;
  • Renderer production build。

Renderer 只有既有的大 chunk 提示,没有构建错误。

全量测试

  • 受限沙箱:2909/2911 通过;
  • 剩余 2 项均为既有 harness-adapter-cancel.test.ts 写入固定 C:\cyrene-test-user-data 时返回 EPERM
  • 允许该固定目录写入后单独复跑:2/2 通过;
  • 综合结果:2911/2911 均已取得通过结果。

明确不包含

  • 插件签名和发布者身份验证;
  • 在线插件市场、仓库索引和自动下载;
  • 自动更新与版本回退 UI;
  • 插件运行时进程隔离;
  • 细粒度网络、文件系统或命令权限;
  • 删除插件私有数据。

上述内容涉及新的信任协议和兼容承诺,建议在 ZIP 本地导入链路稳定后分别设计,避免扩大本 PR 的审查面。

建议审查顺序

  1. src/plugins/installer.ts:路径、资源限制、staging 和原子替换;
  2. src/plugins/manager.ts:串行生命周期、首次停用和失败恢复;
  3. src/main/plugin-runtime.ts:Main Process 文件选择和替换确认;
  4. IPC / Preload / Settings UI:Renderer 权限边界与反馈;
  5. installer.test.ts 与文档:安全回归和对外约定。

# Conflicts:
#	src/main/index.ts
# Conflicts:
#	src/main/channels/bootstrap.ts
#	src/main/channels/init.ts
#	src/main/index.ts
#	src/main/music/shutdown-latch.test.ts
#	src/main/music/shutdown-latch.ts
@Playa-0v0

Copy link
Copy Markdown
Owner

怎么还超额完成了,我先把42关了再review一下

@Playa-0v0
Playa-0v0 merged commit c21236b into Playa-0v0:master Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants