
一个真实的升级动作,和它留下的尾巴假设你按某条 advisory 或某个工具的结论,把 Netty 升到了4.1.136.Final。这个数字不是随手挑的 —— 它是netty-codec-http2上那七条 CVE 的修复版交集:各条的first_patched_version分散在 4.1 线的 124 / 132 / 135 / 136 上,取最大值才是一次修完的那个。升完之后,netty-codec-http2那一侧确实干净了。但你的 jar 树里不止这一个 netty 模块。为什么「一个版本号」会骗人Netty 是全家一起发版的:netty-common、netty-buffer、netty-transport、netty-codec、netty-handler、netty-codec-http、netty-codec-http2…… 同一天、同一个版本号。于是升级在感觉上是一个决定。但漏洞不是按「Netty」编排的,是按模块编排的 ——每个模块有它自己的first_patched_version。io.netty:netty-codec-http上,2025–2026 年这 14 条的 4.1 线修复版是:修复版CVE4.1.125.FinalCVE-2025-58056(low)4.1.129.FinalCVE-2025-677354.1.132.FinalCVE-2026-33870(high 7.5)4.1.133.FinalCVE-2026-41417 · CVE-2026-42580 · CVE-2026-42581 · CVE-2026-42585 · CVE-2026-42587(high 7.5)4.1.135.FinalCVE-2026-500204.1.136.FinalCVE-2026-56746 · CVE-2026-59898 · CVE-2026-59899 · CVE-2026-599214.1.137.FinalCVE-2026-59903七个不同的值。4.2 线同理,是 5 / 8 / 10 / 13 / 15 / 16 /17。一次修完这 14 条,要4.1.137.Final或4.2.17.Final—— 而这个数字,没有写在其中任何一条 advisory 上。它得自己算。关键的一步:两个模块的版本是被 POM 绑死的到这里为止,你还可以说「那是两个模块的事,我只用 HTTP/2」。问题是你用不了「只用 HTTP/2」。打开netty-codec-http2的 POM:https://repo1.maven.org/maven2/io/netty/netty-codec-http2/4.1.136.Final/netty-codec-http2-4.1.136.Final.pom里面这一段:dependencygroupId${project.groupId}/groupIdartifactIdnetty-codec-http/artifactIdversion${project.version}/version/dependency三件事同时成立:compile作用域(没写scope,默认就是它)、非 optional、版本是${project.version}—— 解析为netty-codec-http2自己的版本。也就是说:你拿netty-codec-http2:4.1.136.Final,就一定同时在跑netty-codec-http:4.1.136.Final。两个版本不是「通常一致」,是被 POM 绑成同一个。(我逐个点开核过 4.1.136 / 4.1.137 / 4.2.16 三个版本的 POM,写法完全一致。)于是那条尾巴是:CVE-2026-59903NVD 对这条的描述,第一句就是:Prior to 4.1.137.Final and 4.2.17.Final,io.netty.handler.codec.http.cors.CorsHandlersetVaryHeaderreplaces applicationVaryheaders …Prior to 4.1.137——4.1.136在范围内。所以:按 HTTP/2 那侧算出的4.1.136升上去的人,netty-codec-http这一侧还中着这一条,要4.1.137.Final才算完。4.2 线同型:那侧的答案是4.2.16,这侧需要4.2.17。说清楚这不是谁算错了。按netty-codec-http2这个模块算,4.1.136就是对的。错的是那个很自然的推论:「Netty 升到 4.1.136」和「netty-codec-http2 升到 4.1.136」不是一回事。还有一个更隐蔽的坑:和是混着写的这 14 条 advisory 的版本区间上界,16 处写、12 处写。后果是同一个4.1.136.Final,在两条相邻的 advisory 里判定相反:CVE4.1 线区间4.1.136.FinalCVE-2026-56746 4.1.136.Final安全CVE-2026-59903 4.1.136.Final中招NVD 的描述文本也是这么分的:56746 写「4.1.0.Finalthrough 4.1.135.Final」,59903 写「Prior to 4.1.137.Final」。如果你写脚本批量判这类区间,别把运算符统一成一种:统一成会误报 56746,统一成会漏报59903 —— 而漏报是这类判定最贵的错。不装任何东西的手动自查三步,都能自己复现:① 看你实际装了哪些 netty 模块(不是看 pom —— 用 WebFlux 的话 pom 里根本没有这些坐标):mvn dependency:tree|grepio.netty# 或直接看构建产物unzip-ltarget/app.jar|grepnetty② 逐个模块查它自己的修复版。不要只查一个坐标就下结论。③ 对每个模块,取它身上全部 advisory 的first_patched_version最大值。这一步是纯算术,但没人替你算 —— advisory 是一条一条发的。一个把这三步做完的小工具netty-http-check:扫 jar / fat-jar,列出装着的 netty 模块,对netty-codec-http逐条判这 14 条,给出真正一次修完的版本;检测到你同时装着netty-codec-http2时,把上面那件事明确说出来。https://github.com/xiaoqiMikko/netty-http-check单 jar、零运行时依赖、完全离线、Java 17。java-jarnetty-http-check.jar target/java-jarnetty-http-check.jar myapp.jarjava-jarnetty-http-check.jar --version-of4.1.136.Final退出码:0 没中 ·1 中了 ·2 判不了 ·4 有文件读不动。2和4都故意不是0—— 「什么都没找到」「我没能读它」「很干净」,对脚本来说不能是同一个信号。尤其是4:一个损坏、加密或下载不全的 jar,ZipInputStream读它不抛异常、只给零条目,于是它会安安静静走完整个扫描、得出「没找到」—— 而那句话读起来就是「你不受影响」。「我读不动它」和「你是安全的」必须是两句话。判定表由tools/gen_rules.py从 advisory API 生成,七条断言不过就拒绝出表;其中两条专门守着本文讲的两件事:HTTP/2 的答案在这一侧仍留洞、那个边界仍然判定相反。哪天上游改了口径,生成就会停下来,而不是悄悄出一张看起来对的表。这篇能证明什么、不能证明什么❌不是「Dependabot 看不见这些」。这 14 条全部是 reviewed、包信息齐全、修复版有值,SCA 工具会正常告警。本文讲的是:告警按 advisory 逐条给版本号,而「升到哪个版本才算完」要自己算,尤其在模块之间版本被绑死的时候。❌不是「Netty 有大洞」。14 条里 1 条 low、2 条没有 CVSS 分,其余多数 medium,最高是两条 high 7.5。不要按「又一个大漏洞」读。❌不是「升不上去」。相关版本在 Maven Central 上全部可获取,该升就能升。✅是:同一个版本号之下,不同模块的修完版本不一样;而codec-http2 → codec-http这条依赖把两者绑死,于是只按一侧算的人会留下尾巴。如果你发现哪里错了,欢迎直接开 issue 骂我 —— 这类文章的价值全在准确性上。