一句“为了保持同步”的注释,正好打破了同步 —— Netty 响应错配漏洞实测

先看结论

CVE-2026-42584 影响的是 Netty 的 HTTP 客户端编解码器。如果你的项目里有 io.netty:netty-codec-http,先对一下版本:

版本线 受影响 升到
4.1.x <= 4.1.132.Final 4.1.133.Final
4.2.x 4.2.0.Alpha1 ~ 4.2.12.Final 4.2.13.Final

两个修复版在 Maven Central 上都能直接拉到(我逐个取过 jar,HTTP 200)。 这条不是"升不上去"型的漏洞,它的麻烦在别处:你很可能不知道自己中没中招。

一、三个前提,缺一不可

CVE-2026-42584 的官方 advisory 在 Prerequisites 一节列得很清楚,要同时满足:

  1. 用了 HTTP/1.1 pipelining(一个连接上连续发多个请求,不等前一个响应回来)
  2. 这批 pipeline 请求里有 HEAD
  3. 服务端会发 1xx 响应 (比如 103 Early Hints)

三条缺一不可。所以"扫描器报了"和"你中招了"是两回事 ------ 大多数只把 Netty 当普通 HTTP 客户端、 不开 pipelining 的项目,并不满足条件。

先说清楚这一点,是因为这类文章最容易犯的错就是把话说过头。这个洞真实存在,但它不是所有人都中。

二、中招的时候,到底发生了什么

一句话:上一个请求的响应体,被当成了下一个请求的结果。

具体到那个场景:客户端先后发出 GET /1HEAD /2,服务端依次回

css 复制代码
HTTP/1.1 103 Early Hints

HTTP/1.1 200 OK
Content-Length: 5

hello
HTTP/1.1 200 OK

HttpClientCodec 内部用一个队列保存"已发出但还没收到响应"的请求方法,每收到一个响应就 queue.poll() 弹一个出来配对。问题是:它对 103 这种 1xx 响应也弹了一次。

于是队列错位一格:

  • 103 弹走了 GET
  • 第一个 200(带着 GET 的 hello)配到了 HEAD
  • 而 HTTP 规范说 HEAD 的响应不能有 body,于是解码器跳过不读那 5 个字节
  • hello 就留在了流上,下一个响应从错误的偏移开始解析

结果就是:GET 请求拿不到自己的 hello,而这个连接后面的解析全乱了。

三、我把官方 PoC 在真实构件上跑了一遍

CVE-2026-42584 的 advisory 里自带一段可直接运行的 PoC(用 EmbeddedChannel,不需要起服务)。 我建了个最小 Maven 工程,把它逐字照抄进去,然后只改 Netty 版本号,跑四次:

netty-codec-http 结果
4.1.132.Final(有洞) 通过 ------ 漏洞行为成立
4.1.133.Final(已修) 失败
4.2.12.Final(有洞) 通过
4.2.13.Final(已修) 失败

这里要解释一下为什么"失败"反而是好事:这段 PoC 断言的是有洞时的行为 (它 assert 第二个响应的 body 长度是 0)。所以在修复版上它当然会失败 ------ 失败点恰恰是证据:

yaml 复制代码
DesyncTest.test:51 expected: <0> but was: <5>

多出来的这 5 个字节,正是那句 hello 修复之后,它从错误的响应上回到了正确的响应上。

顺便说一句方法上的事:只跑有洞的版本,只能证明"它会这样",证明不了"是这个洞导致的"。 两个方向都跑,零反例,才算复现。

复现工程很简单,pom 里把版本抽成属性就行:

xml 复制代码
<properties>
  <!-- 用 -Dnetty.version= 切换 -->
  <netty.version>4.1.132.Final</netty.version>
</properties>
<dependencies>
  <dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-codec-http</artifactId>
    <version>${netty.version}</version>
  </dependency>
  <dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-transport</artifactId>
    <version>${netty.version}</version>
  </dependency>
</dependencies>

然后 mvn -Dnetty.version=4.1.132.Final testmvn -Dnetty.version=4.1.133.Final test 各跑一次。

四、根因是一句注释

对比 HttpClientCodec.java 在两个 tag 上的 isContentAlwaysEmpty 方法。

4.1.132.Final:

java 复制代码
// Even if we do not use the method to compare we still need to
// poll it to ensure we keep request / response pairs in sync.
HttpMethod method = queue.poll();

final HttpResponseStatus status = ((HttpResponse) msg).status();
final HttpStatusClass statusClass = status.codeClass();
if (statusClass == HttpStatusClass.INFORMATIONAL) {
    // An informational response should be excluded from paired comparison.
    return super.isContentAlwaysEmpty(msg);
}

4.1.133.Final:

java 复制代码
final HttpResponseStatus status = ((HttpResponse) msg).status();
final HttpStatusClass statusClass = status.codeClass();
if (statusClass == HttpStatusClass.INFORMATIONAL) {
    // An informational response should be excluded from paired comparison.
    return super.isContentAlwaysEmpty(msg);
}

// Get the method of the HTTP request that corresponds to the
// current response.
HttpMethod method = queue.poll();

改动只有一处:把 1xx 的判断挪到了 queue.poll() 之前,让 informational 响应直接返回、不消费队列。

值得停一下的是被删掉的那句注释:

"Even if we do not use the method to compare we still need to poll it to ensure we keep request / response pairs in sync." (即使我们不用它来比较,仍然需要 poll 一次,以确保请求/响应配对保持同步。)

它写的是"为了保持同步",而它做的正是打破同步。 这段代码没有写错 ------ 它精确地实现了那句注释所表达的理解,而那个理解是反的。

五、你该怎么查自己

第一步,确认版本。 Netty 常常是被间接依赖进来的(Spring WebFlux、gRPC、Elasticsearch 客户端等等), 直接翻 pom 可能看不到:

bash 复制代码
mvn dependency:tree -Dincludes=io.netty:netty-codec-http

Gradle:

bash 复制代码
./gradlew dependencyInsight --dependency netty-codec-http

第二步,对照那三个前提。 重点看两个地方:

  • 你有没有显式开启 HTTP/1.1 pipelining(默认不开;很多客户端封装也不用它)
  • 你的上游/CDN 会不会发 1xx ------ 103 Early Hints 是最常见的一种

第三步,不确定就升。 4.1.133.Final / 4.2.13.Final 都是同线小版本,升级成本很低。

最后

CVE-2026-42584 本身不算复杂,但它有个特点值得记一下:它藏在一个"为了正确性"而写的补偿逻辑里。 那句 poll() 之所以存在,正是因为作者担心配对会错位;结果这个担心本身造成了错位。

在解析器里,"多做一步以防万一"和"少做一步导致出错",经常长得一模一样。


相关推荐
吴声子夜歌12 分钟前
Guava——集合(二)
java·guava
疯狂打码的少年17 分钟前
【数据库技术】SQL高级查询(分组/排序/聚合函数)
java·数据库·笔记·sql
DDXYcoder19 分钟前
万字解析动态内存从编译到运行
java·前端·算法
坐吃山猪41 分钟前
【多线程】什么时候必须/推荐使用Thread类
java·开发语言
一只小阿乐1 小时前
java 快速上手开发 2
java·开发语言·前端
骇客野人1 小时前
SpringBoot+SpringCloud高并发系统设计、搭建与落地实施方案
java·spring boot·spring cloud
张宇Joaquin1 小时前
灵渠测试工具-urma_perftest
java·开发语言
当战神遇到编程1 小时前
Java 集合框架详解:Collection 与 List 核心用法
java
张小姐的猫1 小时前
【C++开发脚手架】 —— Jsoncpp上手
java·linux·开发语言·网络·c++·tcp/ip·udp