分发与信任:商店、签名、供应链
前面所有章讲的都是「代码进来之后怎么办」。这一章往前退一步问:这段代码是怎么到用户手上的,中间经过了谁?签名、审核、商店 —— 每一样都只解决一个很窄的问题,而它们合起来也挡不住插件生态最现实的那个威胁:一个好人的插件,被坏人接管了。
把「信任」拆成三个不同的问题
「我能信任这个插件吗」其实是三个完全不同的问题混在一起,而且它们的可解性差别巨大:
| 问题 | 能不能技术解决 | 靠什么 |
|---|---|---|
| ① 完整性:我拿到的,是不是作者发布的那个? | 能,而且很彻底 | 签名、哈希校验 |
| ② 身份:发布它的,是不是我以为的那个人? | 部分能 | 签名 + 账号体系 + 命名空间 |
| ③ 意图:这个人是不是想害我? | 根本不能 | 审核、声誉、权限、时间 |
大多数关于插件安全的困惑,都来自把这三个混为一谈。「我们有签名,所以是安全的」—— 签名解决的是第①个,和第③个毫无关系。
① 完整性:签名能给你的和不能给你的
第 11 章看过 Mihon 的做法:算出扩展 APK 签名证书的 SHA-256,比对信任列表。Android 系统本身还强制了一条更硬的规则 —— 已安装包的后续更新必须由同一把密钥签名,否则直接拒绝。
签名保证的(很可靠):
- 这个包确实由持有那把私钥的人签发;
- 从签发到安装,中间没有被任何人改过一个字节;
- 后续更新来自同一把钥匙 —— 「还是原来那个人」。
签名不保证的(完全不):
- 这个人是善意的;
- 这个人的私钥没有泄漏;
- 这个人的账号没有被盗;
- 这个人没有把项目卖给别人。
第 11 章提过这个词,这里说透。
可归责性(accountability)和防护(prevention)是两种不同的安全性质:
防护阻止坏事发生。沙箱、权限、进程隔离属于这一类。
可归责性不阻止任何事,但它保证事情发生后你能准确知道是谁,并且能把后果绑定到那个身份上。
可归责性的价值在于它让「声誉」成为一种真实的约束。一个作者知道自己的每一次发布都被签名绑定,他就有了长期利益 —— 而攻击的收益通常是一次性的。
但要清楚:它只对「有长期利益的人」有效。对一个用完就跑的攻击者,签名只是让你在事后知道该拉黑哪把钥匙。
② 身份:比签名更难的那一半
签名证明「这个包和上一个包是同一把钥匙签的」。但用户关心的是另一件事:「这把钥匙的主人,是不是我信任的那个人?」
这是一个纯粹的信任引导(bootstrapping)问题,技术无法凭空解决。真实系统的做法:
| 手段 | 怎么工作 | 弱点 |
|---|---|---|
| 商店账号 | 发布者要注册、验证邮箱/付费/实名 | 账号可以被盗、可以被卖 |
| 命名空间 | VS Code 的 publisher.name、Java 的反向域名 | 抢注、仿冒相似名(rnicrosoft) |
| 关联代码仓库 | 插件页面链接到 GitHub,用户可自行核对 | 几乎没人真的去看 |
| 下载量 / 评分 | 社会证明 | 可刷;而且「很多人在用」不等于「安全」 |
| 可复现构建 | 任何人都能从源码构建出字节相同的产物 | 工程难度高,做到的项目很少 |
最后一行是最强的,也是最少见的。可复现构建能把「我信任这个作者」变成「我信任这份公开的源码」—— 因为任何人都可以验证发布的二进制确实来自那份源码。如果你在设计一个高风险的插件生态,这个方向值得投入。
用户搜索「Markdown 预览」,看到两个结果:一个叫 Markdown Preview(真的,10 万下载),一个叫 Markdown Preview+(假的,图标一样,描述抄的,500 下载)。
相当一部分用户会装错。而这类攻击的成本几乎为零 —— 不需要任何技术能力,只需要注册一个账号、抄一段描述。
能做的事(按性价比排序):
① 搜索结果里明确展示发布者和下载量,别只显示插件名;
② 对与已有热门插件名称相似度过高的新插件做自动标记,人工过一遍;
③ 允许作者声明「这是我的官方版本」并做验证徽章;
④ 用户安装一个「新发布、下载量极低、但名字很像热门插件」的东西时,给一个额外的确认。
这些全是产品工作,不是安全工程 —— 但它们挡住的攻击,比任何沙箱都多。
③ 意图:这里没有解
没有任何技术能判断一段代码是不是「恶意的」。因为恶意与否取决于意图和上下文,而代码里没有这些信息。
同样一段「读取本地文件并上传」的代码:在一个备份插件里是核心功能,在一个字体插件里就是后门。代码本身一模一样。
所以这一层只能靠组合拳,每一样都只削掉一部分风险:
自动扫描
Obsidian 会对社区插件的每个版本做自动扫描(安全漏洞、代码质量、恶意代码),结果以「安全评分卡」展示。
能抓到的:已知的恶意模式、明显的混淆代码、可疑的网络地址、和已知恶意样本的相似度。
抓不到的:任何稍微用心隐藏的东西。代码可以从远程拉取、可以延迟触发、可以只对特定用户生效。扫描器和攻击者的关系,和第 14 章那场比赛是同构的 —— 防守方要覆盖所有情况,攻击方只要绕过一次。
但它依然有价值:它提高了下限,挡住了大量低水平的、批量投放的恶意插件。
人工审核
Chrome 应用商店有审核流程。它能挡住一些东西,但:审核者看到的是提交的代码,而运行时的行为可以不同;审核有时间压力;更新版本的审核通常比首次宽松 —— 而这正是下一节那个攻击的入口。
默认不信任
Obsidian 的「受限模式」:新装的应用默认不执行任何第三方插件,用户必须主动关掉这个开关。
Mihon 的「不受信任扩展」:签名不在信任列表里的扩展会以特殊状态显示,需要用户手动确认。
这类设计的价值不在于「用户会认真读那个提示」(他不会),而在于它制造了一个明确的时刻 —— 一个「我现在要开始承担风险了」的分界线。这对后续的责任划分和用户心智都有意义。
最现实的威胁:好人的插件被接管
讲了半天恶意作者,但插件生态里最常发生、危害最大的其实是另一种:
1. 某人写了一个很好用的插件,几十万人在用。
2. 两年后他不想维护了。这时候会发生三件事之一:
a. 有人主动提出「我来帮你维护吧」,他很感激地给了发布权限;
b. 有人提出收购,给了一笔钱;
c. 他的账号被钓鱼/撞库攻破,而他自己都不知道。
3. 新的控制者发布一个「小更新」。
4. 几十万用户的客户端自动更新,恶意代码直接进入所有人的机器。
为什么这个剧本这么有效:
✗ 签名没用 —— 用的是同一把钥匙(他真的拿到了权限)。
✗ 声誉没用 —— 这个插件有两年的良好记录。
✗ 审核大概率没用 —— 更新审核通常比首次宽松,而且恶意代码可以藏在依赖里、延迟几周才触发。
✗ 下载量和评分反而是帮凶 —— 它们让用户更放心。
这就是供应链攻击。它在 npm、PyPI、浏览器扩展生态里都反复发生过,而且是大规模发生。
能做什么
没有根治方案,但有几件真正有效的事:
① 权限是最有效的一道防线 —— 这是第 17 章那条「破坏有限」的真正含义。
如果那个被接管的插件当初只申请了「读写你的笔记」,它就没法读你的 SSH 密钥。攻击者拿到的是一个能力已经被限制死的壳。权限系统防不住恶意作者(他从一开始就会申请他要的权限),但它能极大限制「被接管的好插件」的破坏半径 —— 因为权限是两年前那个善意作者按最小需求申请的。
更进一步:权限变更必须重新征求同意。如果一个更新版本申请了新权限(尤其是危险的那些),必须重新弹窗,而且要突出显示「这是新增的」。Chrome 就是这么做的 —— 权限扩大的更新会被暂停,直到用户确认。这是对供应链攻击最直接的一击。
② 所有权变更要留痕、要公示。如果一个插件的发布者变了,用户应该能看到,而且最好在下次更新时给一个提示。「这个插件换了维护者」是一条用户有权知道的信息。
③ 更新不要全部立刻推送。灰度发布 + 异常监控,能在几小时内发现异常并回滚,而不是等几十万人都中招之后。
④ 要有一个「一键撤销」的开关。能远程把某个版本标记为恶意、阻止它继续运行、并通知已安装的用户。这个能力必须在第一天就建好 —— 出事那天再做就来不及了。
⑤ 关键插件考虑「双人发布」。对下载量特别大的插件,要求发布需要两个人确认。这在开源社区不常见,但对高风险生态是值得的。
换个视角:如果你自己是插件作者
这本书大部分时间站在宿主那一侧。但如果你写插件,上一节那个剧本里你就是第 2 步里那个人 —— 所以有几件事值得单独说。
① 发布密钥比你的代码值钱得多。
代码泄漏了,最多是被抄。发布密钥泄漏了,攻击者能以你的名义把恶意代码推送给你所有的用户。而他们会信任它,因为它确实是你签的。
- 密钥不要放进仓库(哪怕是私有仓库);
- CI 里用密钥库 / OIDC,不要用长期有效的令牌;
- 发布账号一定开双因素认证 —— 这是投入产出比最高的一件事。
② 你的依赖也是你的责任。
你的插件引了一个 npm 包,那个包引了另外八十个。其中任何一个被投毒,恶意代码就会随着你的插件、用你的签名,进到用户机器上。而用户会认为那是你干的 —— 从可归责性的角度说,那确实就是你的责任。
能做的:锁版本(lockfile 要提交)、定期审计、能不引就不引。插件通常很小,为了一个格式化日期的功能引一整棵依赖树,是不划算的风险交换。
③ 只申请你真正需要的权限。
这看起来是利他的,但它首先保护你自己:万一你的账号哪天被攻破,攻击者能造成的破坏被你当初申请的权限限制住了。最小权限是你给未来的自己买的保险。
④ 想弃坑的时候,别把权限随手交出去。
这是上一节那个剧本里最容易被善意促成的一步。如果你真的不想维护了,更安全的做法是归档仓库、在商店里标记为不再维护、建议用户迁移,而不是把发布权限交给一个你只在 issue 区见过的人。
把插件交出去,等于把几十万人对你的信任一起转让了 —— 而那些人并不知道自己被转让了。
如果你没有商店
很多插件系统没有官方商店(尤其是刚起步的),这时候:
做对一件事:让分发渠道可配置。
第 11 章讲过 Mihon 的故事 —— 2024 年 1 月官方仓库因法律压力消失后,生态之所以能延续,正是因为仓库地址是用户可配置的。
如果你把商店地址硬编码进代码,你就给了自己一个致命的单点:服务器挂了、公司倒了、被起诉了、被墙了 —— 整个生态跟着死。
同时做对第二件事:让签名信任独立于分发渠道。因为扩展由作者签名、由系统校验,换一个仓库并不削弱安全性 —— 用户依然能确认扩展来自原作者。
反过来,如果你的信任模型是「商店说它是好的」,那么商店没了,信任链就断了。这两件事的解耦,是插件生态韧性的关键。
装到自己身上
几乎不花钱,但收益极大(今天就做):
□ 记录每个插件的来源和签名/哈希,装的时候存下来
□ 更新时校验签名是否和上次一致,不一致就拦下来问用户
□ 权限变更必须重新征求同意,并突出显示新增的部分
□ 插件列表里显示发布者、来源、安装时间
□ 分发地址可配置,不要硬编码
□ 留一个远程封禁的通道(哪怕只是一个 JSON 黑名单)
中等投入:
□ 自动扫描(哪怕只是几条正则 + 依赖黑名单,也能挡掉一批低水平的)
□ 默认「受限模式」
□ 相似名称检测
□ 灰度发布
高投入(想清楚再做):
□ 人工审核(要有持续的人力预算,做一半比不做更糟 —— 因为用户会以为审核过了就是安全的)
□ 可复现构建
□ 完整的发布者验证体系
无论你做了多少,都要在文档里诚实地写清楚你的安全模型:
插件运行在与本应用相同的进程中,拥有与本应用相同的系统权限。 我们通过签名校验确保插件来自其声明的作者、且未被篡改, 但我们无法验证插件作者的意图。请只安装你信任的作者发布的插件。
这段话让用户能做出真实的决策。而「我们的插件运行在安全沙箱中」(如果那不是真的)会让用户放松警惕 —— 第 14 章说过:最危险的不是没有沙箱,是以为自己有。
Obsidian 在这件事上做得很对:它的文档直白地说明了无法限制插件权限。虽然它的安全模型是五个系统里最弱的,但它的安全沟通是最诚实的 —— 而诚实本身就是一种保护。
这一章的一句话
签名解决完整性、账号体系部分解决身份、而意图无解 —— 所以插件生态最现实的威胁不是「坏人写了恶意插件」,而是「好人的插件被接管」;对付它最有效的不是审核,是当初那个善意作者按最小需求申请的权限,以及「权限变更必须重新同意」这条规则。
卷 V 结束。最后一卷把二十二章的所有零件合起来:完整宿主的代码走查,以及一张你能直接拿去用的决策表。