扫描结果干净,不等于就安全
一句话总结
镜像扫描器只做两个步骤:解开镜像层,生成已安装软件包清单,再把名称和版本与漏洞数据库比对。 它既不运行镜像,也不分析代码。
为什么需要它
如果把扫描器视为魔法黑盒,就会得出两个错误结论:“扫描结果干净,所以很安全”和“发现了 1,247 项,所以必须全部修复”。两者都源于不了解扫描器的工作方式。
理解工作原理后,其局限也自然显现。
(a) 没有软件包元数据,就无法发现。 Debian 系读取 /var/lib/dpkg/status,RPM 系读取 RPM DB,语言运行时则读取锁文件。因此,通过 Dockerfile 中的 curl | tar xz 放入的二进制文件,既不会出现在 SBOM 中,也不会出现在扫描结果中。这是扫描结果干净不等于安全的第一个原因。
(b) 只要出现在清单中,无论是否执行都会报告。 即使应用程序从未调用过 tar,它的漏洞也会照样列出。
工作原理
看实际数字就很直观。扫描 payments:1.4.2 (debian 12.6) 会得到 Total: 1247 (LOW 812, MEDIUM 289, HIGH 137, CRITICAL 9)。加上 --ignore-unfixed --severity CRITICAL,HIGH 后,结果变为 Total: 4,其中与已知被利用漏洞目录(KEV)的交集只有1 项。
总结如下:1,247 项变成了 4 项。安全性没有发生任何变化,只是留下了当前能够处理的项目。
而真正的解决办法不是逐项打补丁,而是更换基础镜像。
| 基础镜像 | 大小 | 检测到的软件包 | 可修复 CRITICAL | HIGH |
|---|---|---|---|---|
| node:22 | 1.12GB | 432 | 9 | 137 |
| node:22-slim | 231MB | 118 | 2 | 24 |
| node:22-alpine | 148MB | 41 | 0 | 3 |
| distroless/nodejs22 | 187MB | 19 | 0 | 1 |
| 静态二进制文件 + scratch | 24MB | 0 | 0 | 0 |
消除 1,247 个漏洞靠的不是打补丁,而是更换基础镜像。 判断标准可以归结为一句话:如果应用实际链接的共享库只有 8 个,镜像里却有 432 个软件包,那么问题不只是漏洞,而是镜像构成。
实际现场中的情况
门禁策略正是从这一困境出发。遇到任何 Critical 都让构建失败,开发就会停滞;不让构建失败,又没人会查看结果。 实际工作中行之有效的折中方案是:只有存在修复版本的 Critical/High 才使构建失败。 如果因为无法修复的问题阻断构建,人们首先学会的会是关闭门禁。任何例外都必须带有到期日(expired_at)。例外悄悄永久化,是门禁失效最常见的途径。
优先级不能只由 CVSS 分数决定。
CVE-2021-44228 epss 0.9444
CVE-2023-45853 epss 0.00412
CVE-2024-5535 epss 0.00196
这三个漏洞的 CVSS 等级都是 Critical。 实际优先级取决于:可利用性 × 暴露程度 × 可达性。
SBOM 格式分为两大类。SPDX 由 Linux Foundation 创建,已成为 ISO/IEC 5962:2021 标准,在许可证合规领域根基深厚。CycloneDX 始于 OWASP,后来成为 ECMA-424,重点是安全漏洞管理。但 SBOM 的真正用途不是归档文档,而是事故发生当天的 5 分钟:新漏洞爆发时,能否立即回答 40 个服务中哪些包含该软件包、分别是什么版本?这一个问题决定了 SBOM 的价值。
把扫描器数字转化为实际风险
首次扫描经常会出现 200 项 CRITICAL。如果原样把清单全部创建为工单,没有人会处理。必须有一个逐步缩小范围的顺序。
第一个问题:该漏洞对应的代码是否真的会在这个镜像中执行? 大多数库虽然存在于基础镜像中,我们的程序却不会调用。现代扫描器可以检查二进制文件是否真正引用相关函数,仅启用这一功能就能大幅缩短清单。
第二个问题:攻击者能否到达该路径? 互联网服务 HTTP 解析器中的漏洞,与内部批处理任务所用压缩库中同分数的漏洞,风险并不相同。CVSS 分数是在不了解具体环境的情况下给出的值,不能直接用来排序。
第三个问题:能否修复? 启用 --ignore-unfixed 会排除尚无补丁的项目。这不是忽视,而是区分现在能做的事与需要持续观察的事。应另建清单跟踪无补丁项目,并在补丁发布当天处理。
trivy image --severity HIGH,CRITICAL --ignore-unfixed myapp:1.2.3
trivy image --format cyclonedx -o sbom.json myapp:1.2.3
大多数漏洞会随基础镜像更换而消失。 约八成漏洞来自我们根本不用的 OS 软件包。换成 slim 或 distroless 镜像,就能一次消除这八成。与逐个处理 CVE 相比,直接更换底座总是更便宜。
定期重新构建比扫描更重要。 镜像会一直携带构建当天的软件包;如果不重新部署,漏洞就会持续累积。即使代码没有变化,只要有每周一次的重新构建流水线,大多数问题就会自动解决。
记录例外时必须附带期限。 如果以“无法修复”为由无限期加入忽略清单,该项目将永久残留。应在 .trivyignore 中同时写明到期日和原因,并让构建在存在过期项目时发出警告。
下一项实验要做什么
亲自制作软件包清单,比较基础镜像与运行时镜像,用数字计算增加的攻击面。手工生成 SBOM 并编写策略门禁脚本,然后确认通过 ENV 放入的令牌会被固化在镜像中,再改为运行时注入。