你的锁定文件是否藏着漏洞?
锁定文件是你代码库里最诚实的一份文档:它列出构建实际安装的每一个软件包的精确版本,没有范围、没有“最新版”、没有意外。它同时也悄悄是一份攻击面清单 — 每一个固定版本,都是一个自你固定它以来、可能已经有人发现漏洞的版本。本文解释扫描器如何把这份清单变成答案:OSV 数据库如何把版本匹配到已知漏洞,为什么 CISA KEV 标志比 CVSS 分数更重要,以及为什么“未发现漏洞”只是关于数据库的一句话,而不是关于你的代码。你可以在SBOM 漏洞扫描器中亲自运行这项检查,它在浏览器内解析你的锁定文件,永不上传。
锁定文件固定了什么 — 以及为什么它就是攻击面
现代软件大多是别人的代码。一个典型的 Web 应用会引入数百个软件包,而其中每一个又会引入更多:对一个 Web 框架的直接依赖,很容易变成两百个你从未选择、也从未审查过的间接软件包。锁定文件就是这棵依赖树的记录 — npm 的 package-lock.json、完全固定的 Python requirements.txt、Go 的 go.mod 和 go.sum — 写下来,是为了让每台机器上的每一次安装都产生相同的依赖树。没有它,“在我机器上能跑”只是一个愿望,而不是事实。
同样的精确性,也正是锁定文件成为查找已知漏洞的正确位置的原因。漏洞数据库以软件包名称、生态系统和精确版本为键:lodash 4.17.20 存在漏洞,lodash 4.17.21 没有,差别只是一次查询。浮动范围没法这样判定 — 扫描器得猜你实际安装的是哪个版本,而猜测不是一种安全实践。所以锁定文件卫生守则的第一条朴实无华、不容商量:提交锁定文件、保持固定,扫描固定下来的依赖树,而不是清单文件里的愿望单。
OSV 如何把版本匹配到已知漏洞
OSV.dev 是一个公开漏洞数据库,把来自 GitHub、NVD 和各语言生态的安全公告汇总成一个可查询的索引。扫描器向它批量发送名称、生态系统和版本的三元组 — lodash、npm、4.17.20 — 然后取回恰好影响该版本的漏洞编号。随后完整获取这些编号:摘要、带修复版本的受影响范围、作为 CVSS 向量的严重程度,以及 CVE 别名。查询中不会带上任何关于你项目的信息 — 只有这些公开软件包的公开坐标。
这种匹配是有意做得“笨”的,而这正是它的长处。它不读你的代码,不判断你是否调用了易受攻击的函数,也不在乎软件包是怎么被使用的。如果安全公告说 4.17.21 之前的版本受影响,而你固定的是 4.17.20,你就匹配上了 — 哪怕你的代码从未触碰过易受攻击的函数。这种过度报告是一个特性:可达性分析是一个更难、更慢的问题,而那个廉价的问题(“这个版本是否已知有问题?”)已经能抓住你三年前忘了升级的那个依赖。
扫描器看不到什么
版本匹配检查有一条硬边界,值得直说:它只能找到已经有人报告过的、针对数据库已知版本的漏洞。昨天才发现、尚未发布的漏洞是隐形的。一个从未有过漏洞、但本身就是恶意的软件包 — 热门名称的仿冒包、被投毒的发布版本、会向外回传数据的依赖 — 根本没有任何 CVE,这类扫描器也不会标记它。你的自有代码也是如此:扫描器评判的是你安装的软件包,而不是你写的代码。
正因为这条边界,“未发现漏洞”这个结果需要仔细解读。它的意思是:在这个数据库里、在此时此刻、针对这些精确版本,没有任何记录。这是一个有用的否定结果 — 一份新锁定文件的干净扫描确实是好消息 — 但它不是一张合格证书。针对扫描器看不到的东西,防御手段是不同的工具:在新依赖进入依赖树之前先固定并审查它,持续关注新的安全公告而不是按季度查看,并对一个沉寂已久的软件包突然发布新版本保持警惕。扫描器是烟雾报警器,不是消防队。
为什么 KEV 标志比高 CVSS 分数更重要
CVSS 分数回答的是一个假设:如果有人利用这个漏洞,会有多严重?CISA 已知被利用漏洞目录回答的是一个事实:是否有人正在野外利用它?这两个答案经常不一致。一个评分为 9.8、但还没有人武器化的漏洞,可以等到下一次计划升级;一个评分为 5.3、但列入 KEV 清单 — 并由 CISA 设定了修复截止日期 — 的漏洞,值得本周就打补丁,因为利用代码已经存在、并且已经在使用中。
正是这种倒置,让扫描器把 KEV 的行单独打上标记,而不是把它们折进严重程度排序里。排序告诉你什么最严重;KEV 标记告诉你什么最紧急。实际的分流很短:先修 KEV 项目,再修已有公开修复版本的危急项,其余的排进下一个升级周期。本站的批量 CVE 分流台把同样的 CVE 放进利用背景中 — KEV 状态、EPSS 概率和 CVSS 一起看 — 当一个标记不足以向团队说明这项工作的必要性时。
提交锁定文件,在 CI 里扫描它 — 而不是一个季度一次
没有提交的锁定文件,就是不存在的锁定文件。如果它只存在于某一位开发者的机器上,其他每台机器安装的,都是当天“最新版”解析出来的版本,而你扫描的那棵树,并不是你交付的那棵树。提交它是最便宜的供应链控制:它让安装可以复现,让扫描器的答案对每个人都成立,并把一次依赖升级变成一份可以审查的差异,而不是悄悄的漂移。
另一半是节奏。漏洞每天都在发布,所以一个季度跑一次的扫描,只是一张几天就会过期的快照。有用的形态是:每次触及锁定文件的拉取请求都跑一次扫描,再加上一份对主分支的定时扫描 — 与测试套件同样的循环,同样的性质:一份新的安全公告会在发布的当天就让绿色的构建变红,那时修复只是一次版本升级,而不是一次事后复盘。本站的浏览器扫描器就是这个循环的手动版本:在 Dependabot 升级之后,或在发布之前粘贴锁定文件,确认你即将交付的那棵树。
修复版本、破坏性变更,以及何时打补丁、何时固定
扫描中的每一次匹配都带有一个修复版本 — 漏洞消失的第一个发布版本 — 或者一句诚实的“尚未发布”。常见情况是补丁版本或小版本升级,升级只是锁定文件里的一行改动。但修复版本并不总是可以安全采用的版本:一次大版本跳跃可能改变你的代码所依赖的行为,而昨天才发布的修复版本,只经过了一天的真实世界检验。判断与任何依赖升级相同:读更新日志、跑测试,先升级你的代码实际导入的软件包。
当没有修复版本时,选择就收窄为三条:用不上的依赖就移除,换一个有人维护的替代品,或者缓解易受攻击的代码路径 — 不再把不受信任的输入传给受影响的函数,或者在安全公告描述的调用前加一道检查 — 并关注安全公告的更新。不应该做的是把易受攻击的版本固定下来然后忘掉它:固定是一个决定,它值得在锁定文件或问题跟踪器里留一句注释,说明原因,并定一个重新审视的日期。做过任何改动后重新扫描;扫描器只需几秒,而第二次扫描是证明第一次扫描的发现已经消失的唯一证据。