先看结论
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 一节列得很清楚,要同时满足:
- 用了 HTTP/1.1 pipelining(一个连接上连续发多个请求,不等前一个响应回来)
- 这批 pipeline 请求里有 HEAD
- 服务端会发 1xx 响应 (比如
103 Early Hints)
三条缺一不可。所以"扫描器报了"和"你中招了"是两回事 ------ 大多数只把 Netty 当普通 HTTP 客户端、 不开 pipelining 的项目,并不满足条件。
先说清楚这一点,是因为这类文章最容易犯的错就是把话说过头。这个洞真实存在,但它不是所有人都中。
二、中招的时候,到底发生了什么
一句话:上一个请求的响应体,被当成了下一个请求的结果。
具体到那个场景:客户端先后发出 GET /1 和 HEAD /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 test 和 mvn -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() 之所以存在,正是因为作者担心配对会错位;结果这个担心本身造成了错位。
在解析器里,"多做一步以防万一"和"少做一步导致出错",经常长得一模一样。