安全部甩来 213 个漏洞,限期两周整改
11 月初,安全部门用他们的商用扫描器对我们的 14 个 Java 服务扫了一遍,发来一份 Excel:213 个"高危及以上"漏洞,要求两周内清零。
我们团队三个人,14 个服务,平均每人每周要处理 35 个漏洞。第一反应是不可能完成。但真正让我头疼的不是数量,是这份报告的质量——我抽查了前十条,有四条根本不成立。
先看扫描报告的问题在哪
服务:order-center
组件:spring-core-5.3.18.jar
漏洞:CVE-2024-22243 严重 Spring Framework SSRF
问题一:版本号识别错了。我们用的是 Spring Boot 3.2,spring-core 是 6.1.x,扫描器靠 jar 包里的 manifest 匹配 CPE,匹配到了 5.3.18。
问题二:不看可达性。报告里有 61 个来自 jackson-databind、commons-collections 这类通用库,但大多数组件我们根本没用到对应的反序列化功能。
问题三:没有上下文。同一个漏洞在不同服务里的风险完全不同。一个内网定时任务服务和一個公网 API 服务,同样一个 RCE,优先级天差地别。
所以我做的第一件事不是修漏洞,是建立我们自己的清单,把扫描结果过滤、分级,再谈整改。
第一步:生成准确的 SBOM
扫描器靠猜,我们靠构建系统。SBOM(Software Bill of Materials)是构建时生成的精确物料清单,包含每一个依赖的坐标、版本和依赖关系。
Spring Boot 3.3 内建了 SBOM 支持,配一下就行:
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<version>2.8.0</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>makeAggregateBom</goal></goals>
</execution>
</executions>
<configuration>
<outputFormat>json</outputFormat>
<outputName>bom</outputName>
</configuration>
</plugin>
生成的 bom.json 长这样,版本是精确的,还带了依赖树:
{
"components": [
{
"type": "library",
"name": "spring-core",
"version": "6.1.11",
"purl": "pkg:maven/org.springframework/spring-core@6.1.11",
"bom-ref": "pkg:maven/org.springframework/spring-core@6.1.11"
},
...
]
}
有了 SBOM,扫描就不再依赖文件名猜测。我们改用 Grype 扫 SBOM,准确率明显提升:
$ grype sbom:./target/bom.json --fail-on high
NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY
tomcat-embed-core 10.1.24 10.1.31 java CVE-2024-23672 High
netty-codec-http2 4.1.111 4.1.112 java CVE-2023-44487 High
logback-classic 1.4.14 1.5.8 java CVE-2023-55222 Medium
第二步:OWASP Dependency-Check 的正确配置
安全部要求必须用 OWASP Dependency-Check,我们保留了它,但做了关键调整——配置 NVD API Key。
这是最大的坑。2023 年底 NVD 开始限速,没有 API Key 的话一次全量更新要跑几个小时,而且经常超时失败,导致本地 NVD 库是几个月前的,漏报严重。
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>10.0.4</version>
<configuration>
<nvdApiKey>${env.NVD_API_KEY}</nvdApiKey>
<failBuildOnCVSS>8</failBuildOnCVSS>
<skipTestScope>true</skipTestScope>
<formats>HTML,JSON</formats>
<suppressionFiles>
<suppressionFile>owasp-suppressions.xml</suppressionFile>
</suppressionFiles>
</configuration>
</plugin>
- API Key 必须配,去 NVD 官网申请,免费。配了之后单次分析从 3 小时降到 12 分钟。
skipTestScope=true:测试依赖不进生产镜像,扫了是噪音。- CPE 抑制文件:解决误报,后面详细说。
处理误报:抑制文件要写理由
<?xml version="1.0" encoding="UTF-8"?>
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">
<!-- 误判:扫描器把 spring-core 6.1.11 匹配到 5.x 的 CVE -->
<suppress>
<notes><![CDATA[
CVE-2024-22243 影响 Spring Framework 6.1.0-6.1.3,当前版本 6.1.11 已修复。
该条为 CPE 版本匹配错误,2024-11-20 由张某某确认。
]]></notes>
<packageUrl regex="true">^pkg:maven/org\.springframework/spring-core@.*$</packageUrl>
<cve>CVE-2024-22243</cve>
</suppress>
</suppressions>
我的原则是每条抑制必须有确认人和日期。抑制文件如果不维护,两年后就是一堆没人敢删的黑洞。我们加了 CI 检查,抑制条目超过 180 天自动告警,要求重新确认。
第三步:分级,不要追求清零
213 条经过 SBOM 重扫和误报剔除,剩 156 条。我按四个维度打分排序:
- 是否公网可达(3 分):API 网关、对外服务 3 分,内网服务 1 分,离线任务 0 分。
- CVSS 评分(3 分):9.0 以上 3 分,7.0-8.9 两分,其余 1 分。
- 漏洞类型(2 分):RCE、SQL 注入、反序列化 2 分;DoS、信息泄露 1 分。
- 是否有已知利用代码(2 分):查 ExploitDB 和 GitHub。
打分之后分三档:
| 档位 | 数量 | 处理策略 | 期限 |
|---|---|---|---|
| P0(8-10 分) | 19 | 立即升版,当天发 | 3 天 |
| P1(5-7 分) | 28 | 排期升版,跟版本一起发 | 2 周 |
| P2(<5 分) | 109 | 登记备案,等大版本升级处理 | 下次大版本 |
P2 那 109 条里,有 87 条来自传递依赖,我们直接依赖里根本没有它们,只能通过升级父依赖间接解决,而父依赖一升级就可能破坏兼容性。对这些我写了风险说明备案,安全部最后接受了。
第四步:自动化升级流程
人工升级 47 个依赖是不现实的,而且这次升完下次还得来一遍。我们把升级做成了流水线。
Renovate 自动提 PR
// renovate.json
{
"extends": ["config:base", ":semanticCommitTypeAll(chore)"],
"packageRules": [
{
"matchUpdateTypes": ["patch"],
"automerge": true, // patch 自动合并
"automergeType": "pr"
},
{
"matchUpdateTypes": ["minor", "major"],
"automerge": false,
"labels": ["needs-review"]
},
{
"matchPackagePatterns": ["^org\\.springframework"],
"groupName": "spring 全家桶" // Spring 相关的合并成一个 PR
}
],
"schedule": ["after 9pm on monday"],
"prConcurrentLimit": 5
}
关键配置说明:
- patch 版本自动合并。前提是单测覆盖率够,我们对核心服务要求覆盖率 70% 以上才开自动合并。
- Spring 相关依赖合并成一个 PR。分开提会出现 spring-core 升了但 spring-web 没升的半吊子状态,启动就报错。
- 每周一晚上跑,避免工作时间刷屏,周二早上统一处理。
升级后的验证流水线
# .gitlab-ci.yml 节选
dependency-upgrade:
stage: verify
script:
- mvn -B verify # 单元测试 + 集成测试
- mvn -B dependency-check:check # 安全扫描
- mvn -B org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom
- grype sbom:./target/bom.json --fail-on critical
- ./scripts/compat-check.sh # 关键 API 兼容性检查
artifacts:
paths: [target/bom.json]
compat-check.sh 是我们自己加的,检查升级后有没有用到被移除的 API。比如 Jackson 2.17 移除了 JsonNode.asText(boolean) 的某些重载,编译能过但运行时 NoSuchMethodError:
#!/bin/bash
# 检查常见的高危 API 变更
if grep -rq "ObjectMapper.enableDefaultTyping" src/main/java; then
echo "ERROR: enableDefaultTyping 已在 Jackson 2.10+ 移除,存在反序列化风险"
exit 1
fi
if grep -rq "new SimpleDateFormat" src/main/java; then
echo "WARN: SimpleDateFormat 非线程安全"
fi
踩过的坑
坑一:升级 logback 导致日志格式全变
把 logback 从 1.4.14 升到 1.5.8 修 CVE-2023-55222,上线后发现我们的日志采集规则全失效了。原因是 1.5.x 改了 %d 的默认时区和 MDC 的输出顺序。
教训:涉及日志、序列化、日期处理的依赖升级,必须在预发环境跑满一个业务周期再上生产。我们现在对这类依赖的灰度周期是 48 小时。
坑二:Netty 升级引发的 HTTP/2 问题
netty-codec-http2 从 4.1.111 升到 4.1.112 修 CVE-2023-44487(HTTP/2 Rapid Reset),升级后有个客户端开始间歇性报 Stream closed before write。查了两天,是客户端的 HTTP/2 实现有 bug,在服务端严格校验之后暴露了。最后是我们给这个客户端加了降级到 HTTP/1.1 的白名单。
坑三:SNAPSHOT 和动态版本
有个内部依赖写的是 1.2.+ 这种动态版本,导致 SBOM 每次生成的版本号都不一样,也没法确定性地复现构建。我们加了 CI 规则,禁止生产依赖使用动态版本:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<executions>
<execution>
<id>ban-dynamic-versions</id>
<goals><goal>enforce</goals>
<configuration>
<rules>
<requireReleaseDeps>
<message>生产依赖禁止使用 SNAPSHOT 或动态版本</message>
</requireReleaseDeps>
</rules>
</configuration>
</execution>
</executions>
</plugin>
两周后的结果
| 项 | 数量 | 说明 |
|---|---|---|
| 原始报告 | 213 | 安全部商用扫描器 |
| CPE 误报剔除 | -41 | 版本匹配错误 |
| 测试依赖剔除 | -16 | 不进生产 |
| 实际待处理 | 156 | |
| 已修复 | 47 | 全部 P0 + 部分 P1 |
| 备案豁免 | 109 | 传递依赖 + 不可达,有书面说明 |
后续每周 Renovate 自动处理 patch 级升级,平均每周自动合并 6.3 个 PR,人工介入 1.2 个。两个月下来,P0 漏洞存量稳定在 0。
下篇预告
这篇先把《Java 项目的依赖安全扫描与漏洞治理》里的坑列了,下一篇写我们当时是怎么在线上工程里真正落地的——包括那次让领导拍桌的故障复盘。