Skip to content

feat(plugin): 插件在线分发的安全模型——签名、审核、封禁 + 补三个加载期漏洞 - #202

Merged
zeweihan merged 5 commits into
masterfrom
claude/plugin-distribution-security
Jul 27, 2026
Merged

feat(plugin): 插件在线分发的安全模型——签名、审核、封禁 + 补三个加载期漏洞#202
zeweihan merged 5 commits into
masterfrom
claude/plugin-distribution-security

Conversation

@zeweihan

Copy link
Copy Markdown
Owner

背景

上一轮讨论确定了方向:学 VS Code 的路子,把安全前移到分发链路。原因是运行时根本拦不住——插件 JAR 与宿主同 JVM 同权限,而 Java 的 SecurityManager 已于 JEP 486 在 JDK 24 永久禁用,OpenJDK 官方给出的替代方案就是进程外隔离。VS Code 与 JetBrains 都明确不做运行时沙箱,靠的是签名 + 审核 + 封禁。

配套官网 PR:aiworkdeck_website feat/plugin-distribution(162a5d1 + e42949e)

设计文档:docs/PLUGIN_DISTRIBUTION.md

一、先补三个加载期漏洞(28e175ec)

这三个与在线分发无关,本地手放插件的场景现在就会踩

  1. 禁用的插件仍会被加载。此前禁用只是把工具从 LLM 可见列表摘掉,JAR 早已 loadClass、静态初始化块与无参构造器都跑过了。管理员点「禁用」的预期是「别运行它」,必须在加载前拦住。现在元数据仍登记(管理页照常展示与启停),但不碰 JAR;运行中启用一个启动时被禁的插件会自动补加载。
  2. backendJars 路径逃逸。manifest 由作者提供,"../../evil.jar" 能指到插件目录外。新增 resolveBackendJar() 用 canonical path 校验(同时挡符号链接)。
  3. pluginSkillDirsCopyOnWriteArrayList——同类集合都因 rescan clear+重填用了并发安全实现,这处是遗漏。

PLUGIN_SPEC.md 同步修正了会让读者高估安全性的措辞:permissions 从「被授予」改为「自行声明」并加显式警告,「独立 ClassLoader」点明父加载器是宿主,补充禁用语义表、rescan 限制、桌面版插件目录落在用户可写的 ~/.aiworkdeck/plugins/

二、在线安装与验签(b3d5e984)

PluginMarketServicePLUGIN_DISTRIBUTION.md §7:

  • 先拉 bundle 用内置 Ed25519 公钥验签,验签失败在下载任何文件之前中止
  • 逐文件下载并比对 SHA-256,任一不符即中止;
  • 全部通过后先写临时目录再原子移动,避免半成品被扫描到;
  • 清单里的相对路径同样做逃逸校验(注册表本身也可能被攻陷);
  • 装完先 markDisabledBeforeLoad 再 rescan——新插件默认禁用,配合第一部分的「禁用即不加载」,用户点启用之前插件代码一行都不会执行。

PluginRevocationService 启动时与每 24 小时同步平台封禁列表,命中的已安装插件强制禁用。封禁不可撤销(setEnabled 拒绝启用被封禁插件,只允许卸载),但不自动删文件——避免误封导致用户数据丢失。

未配置 ai.plugins.registry-public-key 时拒绝一切在线安装(默认留空,需随版本发布填入平台公钥)。

三、广场插件分区(3779c6d2)

从占位换成真实列表,安装前弹确认框列出插件名、作者、声明能力,并写明「插件与本机应用同等权限,能读写你的文件并访问网络。安装后默认停用」。注册表不可达时只在本区块内降级提示。

关于扫描能力(在官网侧)

官网的静态扫描做 class 常量池字符串匹配,覆盖进程执行、TLS 篡改、反射突破、宿主内部访问、网络、文件、反序列化、原生库,另检出硬编码 IP 与明文 HTTP。

最有价值的是交叉验证:把实际引用与 manifest 的 permissions 自述比对,声明与行为不符(如未声明 network 却引用 Socket)会被标出。这让权限声明第一次具备约束力——它不再只是作者的自说自话,而是审核时的对照基准。

验证

  • 后端全量 301 tests 通过;新增 14 个覆盖:路径逃逸拒绝、目录内 JAR 放行、禁用插件登记但不注册工具、验签正误、files 键序无关、未配公钥拒装、非法 id、哈希不符中止且不落盘、验签失败不下载、封禁强制禁用且不可重新启用、semver 比较。
  • 跨语言签名对拍:用官网 Node crypto 签发的签名,桌面端 JDK 验签通过。canonical JSON 差一个字节就会失败,这条必须实测而非推断;用例以 -Dcrosscheck.file 开关控制,CI 无该文件时自动跳过。
  • 官网侧端到端:用 JDK 21 编译了两个测试插件(一个诚实、一个声明零权限却读文件并向硬编码 IP 明文外传),可疑那个命中三条规则 + 两条交叉验证;审核通过后公钥验签通过、篡改版本号验签失败;驳回与封禁的插件 bundle/file 均 404;路径白名单挡住 ../ 与编码绕过。
  • check:emitsbuild:h5 通过;浏览器验证插件分区渲染、已安装/可更新标记、权限标签、注册表不可达的降级路径。

部署前须做

  1. 生成 Ed25519 密钥对,私钥写入官网服务器 AWD_PLUGIN_SIGNING_KEY,公钥填入 application.ymlai.plugins.registry-public-key(未配置时在线安装一律拒绝,属安全默认)。
  2. 官网 PR 部署后 registry 才有 /api/registry/plugins 系列接口。

🤖 Generated with Claude Code

zeweihan and others added 5 commits July 27, 2026 16:10
在线分发上线前先把本地 JAR 链路的三个问题收掉——它们与分发无关,
当前手放插件的场景就会踩。

1) 禁用状态下不加载 JAR。此前禁用只是把工具从 LLM 可见列表摘掉,
   JAR 早已 loadClass、静态块与无参构造器都跑过了。管理员点"禁用"
   的预期是"别运行它",必须在加载前拦住。元数据仍登记,管理页照常
   展示与启停;运行中启用一个启动时被禁的插件会自动补加载其 JAR。

2) backendJars 路径校验。manifest 由作者提供,"../../evil.jar"
   能指到插件目录之外。新增 resolveBackendJar() 用 canonical path
   要求目标落在插件目录内(同时挡符号链接绕行),非法则记 ERROR 跳过。

3) pluginSkillDirs 改 CopyOnWriteArrayList。同类集合都因 rescan
   clear+重填而用了并发安全实现,这一处是遗漏。

PLUGIN_SPEC 同步修正会让读者高估安全性的措辞:permissions 从"被授予"
改为"自行声明"并加显式警告(校验的两个集合都出自作者同一份 manifest,
且声明与行为完全不挂钩,对恶意插件零防御);"独立 ClassLoader"点明父
加载器是宿主、插件可见宿主全部类;补充禁用语义表(能拦未加载的,撤不回
已加载的,处置可疑插件要禁用→重启→移除)、rescan 限制、桌面版插件目录
落在用户可写的 ~/.aiworkdeck/plugins/。

验证:56 tests 全过,新增 3 个覆盖路径逃逸拒绝、目录内 JAR 放行、
禁用插件登记但不注册工具。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
桌面端对接官网的插件分发链路,契约见 docs/PLUGIN_DISTRIBUTION.md。

PluginMarketService 安装流程严格按 §7:先拉 bundle 用内置 Ed25519 公钥
验签,**验签失败在下载任何文件之前中止**;再逐文件下载并比对 SHA-256,
任一不符即中止;全部通过后先写临时目录再原子移动,避免半成品被扫描到。
清单里的相对路径同样做逃逸校验——注册表本身也可能被攻陷。

装完先 markDisabledBeforeLoad 再 rescan:新插件默认禁用,配合上一个提交
的「禁用即不加载 JAR」,用户在广场点启用之前插件代码一行都不会执行。

PluginRevocationService 启动时与每 24 小时同步平台封禁列表,命中的已安装
插件强制禁用。封禁不可撤销——setEnabled 拒绝启用被封禁的插件,只允许卸载;
不自动删文件,避免误封导致用户数据丢失。注册表不可达时静默跳过,不影响本地。

未配置 ai.plugins.registry-public-key 时拒绝一切在线安装(默认留空,
需随版本发布填入平台公钥)。

跨语言签名一致性做了真机对拍:用官网 Node crypto 签发,桌面端 JDK 验签
通过——canonical JSON 差一个字节就会失败,这条必须实测而非推断。
对拍用例以 -Dcrosscheck.file 开关控制,CI 无该文件时自动跳过。

验证:后端全量 301 tests 通过,新增 11 个覆盖验签正误、files 键序无关、
未配公钥拒装、非法 id、路径逃逸、哈希不符中止且不落盘、验签失败不下载、
封禁强制禁用且不可重新启用、semver 比较。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
把插件分区从「即将上线」占位换成真实列表:展示版本、作者、体积、下载数、
工具数与权限自述,已安装的标记出来并在有新版本时给「更新」。

安装前弹确认框,列出插件名、作者、声明能力,并明确写「插件与本机应用同等
权限,能读写你的文件并访问网络。安装后默认停用,需你手动启用」——这句是
整个信任模型的最后一环,用户点确认之前后端不会加载任何插件代码。

区块底部常驻信任提示,说明在线插件都经过人工审核与平台签名、安装时验签。

注册表不可达时只在本区块内提示,不影响 Skill 区块与本地已安装列表
(旧版后端没有 market 接口时同样走这条降级路径,已实测)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PLUGIN_SPEC §3 原本引用「见 §8」,但 §8 是版本演进而非分发章节,改为直接
指向 PLUGIN_DISTRIBUTION.md;§8 补记 v2.2(加载期收口 + 在线分发落地)。

AI_ARCHITECTURE 的「插件进程级运行时沙箱」TODO 改为已完成的分发链路安全 +
未来的进程外形态(MCP server),并显式写明进程内沙箱不可行的原因
(JEP 486 在 JDK 24 永久禁用 SecurityManager,OpenJDK 官方推荐进程外隔离)。
留着这条 TODO 会让人反复去追一个做不到的目标。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@zeweihan
zeweihan merged commit 98cff2a into master Jul 27, 2026
4 checks passed
@zeweihan
zeweihan deleted the claude/plugin-distribution-security branch July 27, 2026 09:34
zeweihan added a commit that referenced this pull request Jul 28, 2026
自 v0.8.0 累积 19 个 PR(#196#216)。升 minor 而非 patch 的理由:
在线插件分发链路本版首次真正可用(#202 安全模型 + #203 生产签名公钥),
插件广场提级到左栏并引入 Skill 三档生效方式(#198),属新增用户可见能力。

发版前回归(本 worktree 冷启动):
- backend mvn -B test:318 通过 / 0 失败 / 2 跳过(JDK 21)
- desktop npm test:14/14
- frontend check:emits:通过
- app-e2e:30/30(基线 29,#216 新增剪贴板面板一步)
- lowa-e2e:38/38(持平 v0.8.0 基线)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
zeweihan added a commit that referenced this pull request Aug 27, 2026
- #200 充值弹窗与账户卡重设计:RechargeDialog 视觉重做(副标题/档位卡/
  自定义金额前缀框/带金额主按钮/轮询呼吸点);账户卡改为「身份一行 +
  动作按齐」(头像/显示名/用户名 + 刷新权益/退出登录)
- #201 剪贴板「立即解锁」扣费后未解锁:UnlockHint 购买成功(含 already_owned)
  广播 awd:entitlements-changed,ClipboardPanel 与暂存区用量订阅重拉——
  hiddenCount/limited 是后端算的,不重拉横幅停在旧值
- #202 「优化者」卡对普通用户隐藏:桌面单机人人 isAdmin 挡不住,改按
  optimizer.enabled(这台机器配没配 optimizer.*)门控
- #203 工作台空态美化:品牌图标白色圆角砖 + 淡绿光晕 + 标题/引导副文案
- #204 左 rail 图标拖动排序:railSort.js(awd_rail_order 全局键),
  applyRailOrder 套在停靠过滤之后,与跨 dock 停靠拖拽共存
- #205 退出/断开入口统一:「断开连接」废除,账户卡与顶栏头像下拉(恢复为
  设置/退出登录两项)都走 utils/signOut.js 唯一编排;「解除授权」补说明文案
- #206 设置里「插件广场」导航项删除:入口收敛到左 rail 插件中心,
  /pages/plugin-market 独立页保留给直链

契约同步:check-navigation-contract 头像下拉断言更新为「恰好两项」;
app-e2e J6.3 同步;五份领域文档(sidebar-shell/licensing-billing/
utility-tools/feedback-optimizer/plugin-marketplace)随改动更新。

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant