Administrator
发布于 2024-11-27 / 3854 阅读
33

Java 项目的依赖安全扫描与漏洞治理

安全部甩来 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-databindcommons-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 条。我按四个维度打分排序:

  1. 是否公网可达(3 分):API 网关、对外服务 3 分,内网服务 1 分,离线任务 0 分。
  2. CVSS 评分(3 分):9.0 以上 3 分,7.0-8.9 两分,其余 1 分。
  3. 漏洞类型(2 分):RCE、SQL 注入、反序列化 2 分;DoS、信息泄露 1 分。
  4. 是否有已知利用代码(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 项目的依赖安全扫描与漏洞治理》里的坑列了,下一篇写我们当时是怎么在线上工程里真正落地的——包括那次让领导拍桌的故障复盘。

参考