pom 里没有、代码里没调过,但 WebClient 默认用的就是 netty-resolver-dns

一条你看不懂的告警

依赖扫描报出来一个包:io.netty:netty-resolver-dns,三条 CVE,名字里带着「DNS 缓存投毒」。

你去翻 pom.xml,没有这个坐标。去全局搜代码,没有一行 DnsNameResolver。 很自然的反应是:我们又不做 DNS,这是某个库顺带进来的,应该用不到。

这篇要说的就是:如果你用 Spring WebClient,这个直觉大概率是错的 ------ 而且可以当场验证。

先把三条编号摆出来:

CVE 问题 严重度
CVE-2026-45674 CNAME 记录没有校验来源(bailiwick) GitHub advisory 8.7 high · NVD 10.0 critical
CVE-2026-47691 NS 记录来源校验不足,子域的权威服务器能投毒上级域 GitHub advisory 8.7 high · NVD 10.0 critical
CVE-2026-45673 事务 ID 可预测 + 默认固定 UDP 源端口 6.8 medium

三条共用同一个修复版:4.1.135.Final (4.1 线)/ 4.2.15.Final(4.2 线)。

它是怎么进来的:四层传递,每一层都是 compile

markdown 复制代码
spring-boot-starter-webflux
 └ spring-boot-starter-reactor-netty
    └ reactor-netty-http
       └ reactor-netty-core
          └ netty-resolver-dns

这条链我在 repo1.maven.org 上逐个 POM 点开核过:每一环都是 compile 作用域、都不是 optional。 所以只要你引了 starter-webflux,它就一定在你的 fat jar 里 ------ pom 里看不到,是因为它根本不需要你写。

进来了不等于在用 ------ 但这里它默认就在用

在 classpath 上躺着,和真的被调用,是两回事。很多传递依赖确实只是躺着。

这个不是。Reactor Netty 的 HttpClient 解析主机名,默认走的是 Netty 自己的 DnsAddressResolverGroup,不是 JDK 的解析器。源码路径:

  • HttpClientConfig.defaultAddressResolverGroup()HttpResources.getOrCreateDefaultResolver()
  • TcpResources.getOrCreateDefaultResolver()NameResolverProvider.newNameResolverGroup(...)
  • NameResolverProvider.newNameResolverGroup 里:new DnsNameResolverBuilder()new DnsAddressResolverGroup(builder)

(reactor-netty 1.1.x、1.2.x、1.3.x、main 都是这样。)

而 Spring Boot 给 WebClient 挑连接器的顺序是 reactor > jetty > httpComponents > jdk ------ classpath 上有 Reactor Netty,就选它。

读源码还不够,我做了运行时验证。一个默认配置的 Spring Boot 3.5.14 应用,WebClient.builder() 发一次请求,打开 io.netty.resolver.dns 的 DEBUG 日志,里面有这一行:

yaml 复制代码
DEBUG --- [ctor-http-nio-1] io.netty.resolver.dns.DnsQueryContext : [id: 0xc823de72, L:/[0:0:0:0:0:0:0:0]:53628] WRITE: UDP, [3313: /198.18.0.2:53], DefaultDnsQuestion(example.com. IN A)

这就是 Netty 的 DNS 解析器亲手发出去的查询。 这个应用的代码里没有一个字提到 DNS。

哪些 Spring Boot 版本默认中招

从每个 spring-boot-dependencies-<版本>.pom<netty.version> 逐个读出来的:

Spring Boot 默认 netty 结论
3.4.0 -- 3.4.13 4.1.115 -- 4.1.130 中 ------ 3.4 线的最后一版 3.4.13 也没带上修复版
3.5.0 -- 3.5.14 4.1.121 -- 4.1.132
3.5.15 起 4.1.135 已修
4.0.0 -- 4.0.6 4.2.7 -- 4.2.12
4.0.7 起 4.2.15 起 已修

两个注意点:如果你在 pom 里自己覆盖过 netty.version,以你覆盖的为准;3.4 线在线内升补丁版修不掉,要么覆盖 netty.version,要么升到 3.5.15+。

静态扫描能判到哪一步:五个对照应用

光看版本号只回答了一半。另一半是:你的应用有没有把这个解析器换掉? 有人会写 .resolver(DefaultAddressResolverGroup.INSTANCE),也有人用配置把 WebClient 的连接器整个换成 JDK 的。

我做了五个真实的 Spring Boot fat jar,标准答案用运行时实际发出的 Netty DNS 查询条数,再拿静态扫描的结论去对:

应用 做法 运行时 Netty DNS 查询 静态扫描结论
A 默认 WebClient.builder() 1 在用
B .resolver(DefaultAddressResolverGroup.INSTANCE) 0 发现覆盖痕迹 → 请自查
C spring.http.reactiveclient.connector=jdk 0 发现覆盖痕迹 → 请自查
D 两个 WebClient,只换了其中一个 1 发现覆盖痕迹 → 请自查
E 默认配置,Spring Boot 3.5.16 1(但已是修复版 4.1.135) 不受影响

老实说结果:

  • 没有覆盖的时候,静态扫描能可靠地说「在用」;
  • 有覆盖的时候,它能找到覆盖的痕迹(3 个里找到 3 个,A 上没有误报);
  • 但它分不开 B 和 D ------ 一个全换了(0 次查询),一个只换了一半(还有 1 次查询)。两者在字节码里的证据完全一样。

所以正确的做法是:找到覆盖痕迹时,不下「不受影响」的结论,而是让你去自查。 判错的方向必须选对 ------ 把在用判成没用,是让人做错事。

一分钟自查:它到底有没有在发 DNS 查询

  1. 启动应用时加上 --logging.level.io.netty.resolver.dns=DEBUG
  2. 让应用发一次出站请求 ------ 有几个 WebClient,就各触发一次
  3. 在日志里找 WRITE: UDP:有,就是 Netty 的解析器在用;没有,这次请求没走它

⚠️ 别拿「DnsNameResolver 这个类有没有被加载」来判。 我一开始就是这么判的,结果应用 B 把解析器全换掉了,照样加载了这个类 ------ 默认解析器组是预先构建出来的。只有真实发出的查询才算数。

怎么修

  • 把 netty 升到 4.1.135.Final(或 4.2 线 4.2.15.Final)以上。Spring Boot 项目在 pom 的 <properties> 里设 <netty.version>,Gradle 设 netty.version 属性。
  • 或者升 Spring Boot:3.5.15 起、4.0.7 起默认带的就是修复版。
  • 修复版在 Maven Central 上都拿得到,正常升级即可。

利用有前提,别读过头

  • CVE-2026-47691 的 advisory 原文:控制某个子域权威 DNS 服务器的攻击者,可以投毒它的上级域。换句话说,你的应用得去解析一个攻击者控制得了的域名 ------ 比如按用户给的 URL 去发请求、抓取回调地址这类场景。
  • CVE-2026-45673medium,它降低的是伪造 DNS 响应的难度,前提是攻击者能把伪造的响应送到你的解析器。
  • 严重度两套分数都在上面的表里:GitHub advisory 给两条 8.7 high ,NVD 自己打的是 10.0 critical ;第三条只有一个分,6.8 medium
  • 没有做过利用复现。本文证明的是「它在被用、版本在不在窗口内」,不是「你的应用一定能被打穿」。

一个把前面这些做完的小工具

netty-resolver-dns-check:扫 jar / fat jar / war(读的是实际打进去的 BOOT-INF/lib,不是 pom),判版本在不在窗口内、Reactor Netty 在不在、你的代码和 application*.properties/yml 里有没有换掉解析器或连接器的痕迹,然后给出三档结论之一,附上面那个一分钟自查。

github.com/xiaoqiMikko...

单 jar、零运行时依赖、完全离线、Java 17+。

bash 复制代码
java -jar netty-resolver-dns-check-0.1.0.jar myapp.jar
java -jar netty-resolver-dns-check-0.1.0.jar target/
java -jar netty-resolver-dns-check-0.1.0.jar --version-of 4.1.132.Final

退出码:0 = 不受影响 · 1 = 中了(包括发现覆盖痕迹的情况2 = 判不了 · 4 = 有文件读不动。 24 故意不是 0:「我没找到」「我没能读它」和「你是安全的」,对 CI 脚本来说不能是同一个信号。

仓库里的 evidence/ 就是上面五个对照应用的源码,附一个数 WRITE: UDP 的脚本;tools/e2e_real_jars.py 拿这五个真 jar 跑工具、逐个断言结论。你可以把整个对照实验在自己机器上重跑一遍。

这篇能证明什么、不能证明什么

  • 不是说扫描工具报不出来。 三条都是 reviewed、有修复版的 advisory;GitHub 的官方文档写着 Maven 的传递依赖在依赖图里是支持的。报是报得出来的,本文讲的是报出来之后「我没用它」这个判断为什么站不住。
  • 不是「升不上去」。 修复版在 Central 上都有。
  • 不是「任何 WebClient 应用都会被劫持」。 利用有前提,见上一节。
  • :这个包你没写进 pom、没在代码里调过,但 WebClient 默认就经它解析域名;而静态扫描能帮你判到「在用 / 请自查」这一步,判不到「全换干净了」------ 那一步要靠那一分钟的日志。

如果你发现哪里错了,欢迎直接开 issue ------ 这类文章的价值全在准确性上。

相关推荐
wuminyu1 小时前
Kafka的写入延迟与磁盘IO抖动原理分析
java·linux·c语言·jvm·c++
aramae1 小时前
模拟实现strcpy(字符串拷贝)(C语言)
java·c语言·开发语言·算法
IT毕设实战小研1 小时前
基于大数据的跨国外派人员适应满意度与留存影响因素可视化分析
android·java·大数据·python·django·课程设计
letisgo51 小时前
JAVA 高级进阶10篇《消息队列实战:RocketMQ/Kafka选型与“不丢不重有序“三连解》
java·面试·kafka·消息队列·rocketmq
2401_850481171 小时前
UVA101
java
Lyyaoo.1 小时前
【回溯】【中等】全排列
java·数据结构·算法
xixingzhe21 小时前
spring boot对接Dify
java·spring boot·dify
数据狐(Datafox)1 小时前
京东商品详情API接口解析(附 JSON 样例)
java·开发语言·json
步行cgn1 小时前
Spring Boot 指定数据来源详解
spring boot·后端·python