你引用的每一个包,都在你的构建里拥有与你相同的权限。
一、一条被标成低危的告警
三月的第一个周一,扫描器照例生成周报:186 条告警,11 条高危、47 条中、128 条低。司南花四十分钟把高危看完,升级了三个包,剩下的按级别归档。其中一条低危来自一个间接依赖,包名和他常用的那个只差一个连字符。
两周后他做 typosquatting 演练,把几个形近包名放进一个空项目安装。其中一个在安装脚本里发起了一次外部请求,虽然只发到一个他自己控制的地址,但足以说明问题:那条被他归档的低危,指向的是同一种包。
以下情境为教学假设,人物、引语和数字不代表经核实的真实案例。
供应链的风险不在你选的包,在你没选的包。 41 个直接依赖是他逐个看过 README 才加的;约 380 个间接依赖,他一个都没主动选过,却全部进了构建、全部能执行安装脚本。
告警的级别由漏洞评分决定,不由你的用法决定。 一个在测试工具里被标低危的反序列化问题,如果那个工具在 CI 里跑,风险和标高危的没有区别。
安装脚本是供应链最容易被忽略的执行点。 包被安装时会跑代码,权限与构建进程相同;只读源码不跑安装,等于只看了一半。
演练比告警更能让人记住。 一条低危躺两周没人看,一次自己动手的安装演练,十分钟就让人明白低危也可能是入口。
二、41 与 380:间接依赖才是大头
直接依赖是他写进清单的,间接依赖是清单自己长出来的。两者的可见性差一个数量级。
| 层级 | 数量 | 谁选的 | 可见性 | 处理方式 |
|---|---|---|---|---|
| 直接依赖 | 41 | 他 | 逐个评估过 | 锁版本,升级需看变更 |
| 间接依赖 | 约 380 | 上游决定 | 只在锁文件里 | 锁文件入库,定期扫描 |
| 传递更深层 | 计入 380 | 上游的上游 | 几乎不可见 | 只依赖扫描与最小化 |
41 个直接依赖已经算克制。 每加一个包,等于把它的全部依赖树一起引进;评估一个包时,看的是它的依赖数量与最近一次发布时间,不只看 star 数。
锁文件必须入库,且必须能复现。 同一份清单在两次安装里装出不同版本,扫描结果就没有意义;npm ci 这类严格按锁文件安装的命令要成为唯一入口。
间接依赖的升级由上游驱动,不由他决定。 他能做的是:锁定顶层版本、定期扫描、在出现高危时用覆盖机制临时顶住,再等上游跟进。
依赖越少,攻击面越小,但不是越少越好。 自己实现加密、解析、时区,出错概率远高于用一个成熟包;判断标准是「这个包是否只做一件事,且维护活跃」。
每个季度删一轮。 列出 30 天没有被 import 的包,确认后移除;未使用的依赖仍然是依赖,仍然会被安装、仍然会跑脚本。
许可证也要顺手看。 扫描器只管漏洞,不管许可;一个间接依赖换成 AGPL,分发时就多一层义务,具体以专业意见为准。
安装来源要固定。 只从官方 registry 装,配置镜像时确认镜像的同步来源;一个被替换的镜像,能把整棵树换成别的。
三、186 条告警,只修 23 条
全修既不现实也没必要,关键是每一条都有明确去向:修、记录、或登记忽略。
| 级别 | 数量 | 修复数 | 单条耗时 | 处理依据 |
|---|---|---|---|---|
| 高危 | 11 | 11 | 25 分钟 | 有修复版本就升级,无则临时覆盖 |
| 中 | 47 | 12 | 15 分钟 | 只修可达路径上的 |
| 低 | 128 | 0 | 1 分钟记录 | 记录影响面,不主动修 |
| 忽略登记 | — | — | 共 35 分钟 | 写清原因与复查日期 |
高危 11 条全修,没有例外。 25 分钟是平均时间,包含升级、跑测试、看变更日志;有两条需要连带升级三个包,实际花了 50 分钟,也从别处省回来。
中危只修可达的 12 条。 47 条里大部分位于未使用的功能或仅开发环境;判断「可达」靠一条 grep:这个包的入口函数有没有被调用过。
低危只记录,不修。 128 条平均 1 分钟,写清包名、影响面、为什么现在不修;记录的价值在于下次同类告警出现时不用重新判断。
忽略项要登记,不能静默。 35 分钟登记了 9 条永久忽略项,每条写明「为什么可以忽略」与「什么时候复查」;没有登记的忽略,半年后会被当成新问题重查一遍。
处理率 23/186 约 12.4%,这是设计而非偷懒。 把 186 条全修,时间会从 10.3 小时涨到几十小时,结果是下个季度没人再看报告。
告警要闭环到人。 每周固定 20 分钟过一遍新增,标记去向;超过两周没有去向的告警,升级为待办。
同一个包可能被算成两条。 它在直接与间接两处出现时,报告会各算一次;按包名去重后再算处理率,数字才可比。
四、账:618 分钟与一次 40 小时的事件
修复成本是可以算的,事件成本只能估;两个数放在一起,才知道该花多少时间在预防上。
复算:
告警 186 = 高危 11 + 中 47 + 低 128
高危 11 × 25 分钟 = 275 分钟
中危修 12 × 15 分钟 = 180 分钟
低危 128 × 1 分钟 = 128 分钟
忽略项登记 = 35 分钟
合计 275 + 180 + 128 + 35 = 618 分钟 = 10.3 h
加每月审计 20 分钟 × 12 = 240 分钟 = 4 h
全年 = 10.3 + 4 = 14.3 h
处理率 = 23 ÷ 186 ≈ 12.4%
一次供应链事件按 40 h 计 → 14.3 h 的预防只要挡下 0.36 次就回本
40 小时是一次教学假设。 它包含定位被污染的版本、回滚构建、轮换构建密钥、通知受影响客户;真实事件可能更长,也可能只是一次虚惊。
14.3 小时占 1,920 小时的 0.74%。 这个比例低于攻击面盘点加密钥轮换,但它防的是唯一能一次影响所有客户的入口。
回本点很低,所以不值得讨价还价。 一年挡下不到半次事件就够本;真正的约束不是时间,是每周那 20 分钟能不能稳定空出来。
可复现构建是这条账的地基。 锁文件、固定工具链版本、记录构建产物的摘要;没有这些,出了问题连「装的是哪个版本」都答不上来。
演练要包含安装阶段。 把形近包名放进空项目,观察安装脚本是否发起外部请求;一次十分钟的演练,比读十篇供应链文章有效。
每次部署记下产物摘要。 产物 digest 与锁文件哈希一起留存;出事时能确认线上跑的到底是哪一版,回滚也有依据。
五、行动清单与复算验收
先把锁文件与扫描固定下来,再谈处理率。
- 锁文件入库,安装只走严格按锁文件的命令。
- 每周固定 20 分钟过新增告警,每条标记去修、记录或忽略。
- 高危 11 条全部升级,无修复版本的写临时覆盖与复查日期。
- 中危只修可达路径,用一次调用检索确认可达性。
- 低危逐条记录影响面,不主动升级。
- 建一张忽略登记表,写原因与复查日期。
- 每季度列出未使用依赖并移除。
- 做一次 typosquatting 安装演练,记录安装脚本行为。
验收标准:每条告警都有去向,忽略项都有复查日期;抽查三条,能说清为什么修或不修。
使用边界与实操验收
依赖管理处理的是「别人写的代码进了你的构建」,不覆盖自己代码里的漏洞。
扫描器只报已知漏洞。 没有 CVE 的恶意包、刚投毒还没被收录的版本,扫描器都不会报;安装脚本演练与依赖最小化补的是这一块。
自动升级不是默认安全。 自动合并小版本能减少告警,也会把未经审查的代码直接送进构建;高危可以自动提 PR,合并仍然要跑测试并看一眼变更。
可复现构建有成本。 固定工具链、记录摘要、维护制品库,都要时间;对一个人来说,做到「同一把锁文件装出同一棵树」已经足够,不必追求逐字节可复现。
供应链事件不总是技术问题。 包被收购、维护者失联、许可证变更,都会影响可用性;这些不在漏洞库的覆盖范围内,只能靠季度复核发现。
本文不构成合规结论。 依赖清单、漏洞处置时限与软件物料清单的要求因行业与司法辖区而异,以专业意见为准。
关键参考与核验范围
链接只用于核对一般做法,具体配置以各自工具链为准。
- GitHub Dependabot 文档 :用于说明依赖告警与自动升级 PR 的机制。
- SLSA :用于说明构建来源与可复现性在供应链中的位置。
- CISA 已知被利用漏洞目录 :用于说明「已被实际利用」应优先于评分。
- CWE Top 25 :用于说明常见缺陷类型与依赖漏洞的区分。
以上链接于 2026-10-26 实测可公开访问;未核验版本、功能范围与政策更新。核验日期:2026-10-26。
范围上只讨论直接与间接依赖的盘点、告警分级与修复成本,不讨论制品签名、私有 registry 搭建与供应商审计;那些属于更重的供应链题目。
系列导航
- 上一篇:输入与输出:注入、XSS、SSRF 与文件上传的四条边界
- 下一篇:限流与滥用:四个配额与一次被刷的导出
- 回到目录:一个人的安全基线:十四个入口、十七个密钥与一次 72 小时响应
- 相关阅读:依赖与升级:四十一个直接依赖的取舍
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。