受影响的是怎样的解码输入
Square 的 Wire 运行库(com.squareup.wire:wire-runtime)在解码 protobuf 消息时存在长度计算溢出,畸形输入可能使请求处理或解码过程失败。protobuf 是将结构化数据编码为字节的二进制格式,Wire 负责按消息定义把字节还原为字段值。CVE-2026-63126 的关键出在 JVM(Java 虚拟机)读取器 ByteArrayProtoReader32:它将过大的长度与当前读取位置相加,结果溢出,使本应被拒绝的输入继续进入读取操作,最终抛出未受检的运行时异常。
攻击者必须能让应用使用受影响的 Wire 运行库解码其控制的 protobuf 字节。JVM 的 ProtoAdapter.decode(ByteArray) 和 ProtoAdapter.decode(ByteString) 字节输入接口会使用这一快速读取路径,其中 ByteString 表示字节串。远程触发还要求应用有可达的网络输入,并将这些字节交给解码器;Wire 本身是库,不自带 HTTP 服务或解码端点。只有输入无需认证即可到达时,攻击者才不需要认证;存在认证网关时仍须通过网关。触发不需要用户交互。
ProtoAdapter.decode() 约定通过 IOException 报告解码失败。它属于 Java 的受检异常,调用方需要捕获或声明;此漏洞却让 IllegalArgumentException 或 ArrayIndexOutOfBoundsException 这样的未受检运行时异常逃出接口,绕过只捕获 IOException 的错误处理分支。因此,直接风险是解码或请求处理失败;是否终止整个进程、是否持续不可用,还取决于应用如何处理异常以及如何恢复或重启。
该 CVE 及关联公告将影响归于可用性,未报告机密性、完整性或代码执行影响。
正数长度为何绕过边界检查
protobuf 的“长度分隔字段”先给出字段标签和内容长度,再放置相应字节,字符串就是其中一种。读取器需要确认声明的内容没有越过外层消息的末尾。pos 是当前读取位置,limit 是当前字段或消息的结束位置;进入字段时,pushedLimit 保存外层边界。Wire 6.4.1 的以下源码使用 Int,也就是有符号 32 位整数,保存这些位置。重点在于 internalNextLengthDelimited():它用 varint(变长整数编码)读出长度,只排除负数,就先做加法、后比较边界。
Wire 6.4.1 在边界校验前累加长度
internal class ByteArrayProtoReader32( |
原始记录节选。
六字节字符串样例只含字段标签和长度声明。读完后,pos = 6,声明长度为 2147483647,即有符号 32 位整数的最大值。按上述源码计算,6 + 2147483647 超出可表示范围,回绕为 -2147483643。新 limit 因而变成负数,limit > pushedLimit 为假,原本应抛出 EOFException 的检查被绕过。EOFException 是 IOException 的子类,表示所需数据超出可读取范围。攻击者不必实际提交与声明长度相当的数据。
错误边界还会影响下一步。beforeLengthDelimitedScalar() 计算 limit - pos,再次回绕得到正数 2147483647,随后将 limit 恢复为外层边界。readUtf8() 用这个字节数再次计算 pos + byteCount,得到负的 newPos;它仍不大于恢复后的外层边界,于是第二道检查也失效。最终,decodeToString() 收到起点为 6、终点为负数的无效区间,抛出 IllegalArgumentException。这是源码中的整数运算所导出的路径,后面的异常记录与这些索引吻合;源码本身并不决定宿主进程如何处理异常。
相同类别的边界错误还涉及未知字段读取和 group 跳过。未知字段是当前消息定义未识别的字段,读取器可用 ByteString.of 保存其字节;group 是 protobuf 的一种字段分组结构,skip() 用于跳过内容。错误的长度或读取位置进入这些操作后,会造成越界。Wire 6.4.1 的另外两种完整溢出输入分别在这些路径产生了 ArrayIndexOutOfBoundsException。
解码器异常与进程退出要分开看
下面的 Java 代码来自调用 Wire 的自定义 HTTP 测试程序。Person 是示例消息类型,Person.ADAPTER 是与这一类型对应的消息适配器,负责按字段定义把 protobuf 字节解码为 Person 对象。处理函数将请求体交给 Person.ADAPTER.decode(ByteArray);重点看两个异常分支:IOException 被转为 HTTP 400,其他 Throwable 一旦逃出解码调用,程序就显式执行 Runtime.getRuntime().halt(1),以状态 1 终止 JVM。
测试程序将未受检异常转换为 JVM 退出
static final String BANNER = |
原始记录节选。
这段代码展示了测试程序如何把未受检异常转为进程退出,这不是 Wire 自带的服务或终止策略。实际应用若采用不同的异常处理方式,结果可能只限于一次请求失败。代码中的版本横幅是固定字符串,替换运行库后也不会自动改变。
以下交互记录中的 <TARGET>、<PATH> 和 <path> 分别是目标、文件位置与请求路径的替换标记,不能直接当作可执行命令使用。记录未给出整段 shell 的退出状态,文中只解释明确记录的子命令结果。
先看加载 Wire 6.4.1 的测试程序对正常输入的处理。合法的 Person 消息用于检查成功解码;另一条输入声明字符串长 5 字节,却只提供 3 字节,用于检查普通截断是否进入受检异常分支。这是与后面溢出交互分开的本地对照记录。
合法输入和截断输入展示正常处理行为
printf '\x0a\x05hello\x10\x2a' | curl -s -m 5 --data-binary @- http://<TARGET>:8080/decode; echo "code=$?"; echo "---truncated benign---"; printf '\x0a\x05hel' | curl -s -m 5 -w '\nHTTP=%{http_code}\n' --data-binary @- http://<TARGET>:8080/decode; curl -s -o /dev/null -w 'still-alive GET=%{http_code}\n' -m 5 http://<TARGET>:8080/ |
节选;占位符替换了实验专用值,未修改代码逻辑或结果。
合法输入得到 Person{name="hello", birthYear=42}。截断输入则产生 EOFException,测试程序返回 HTTP 400,随后的 GET 返回 200,说明这个错误请求没有使程序停止响应。这里的 code=0 只是第一条 curl 的退出状态,不是 HTTP 状态码;这两项对照也不能证明溢出输入会被同样拒绝。
再看前述六字节溢出输入:它将字符串长度声明为 2147483647,并未提供对应内容。在新启动、加载 Wire 6.4.1 且采用上述退出策略的本地测试程序中,将这条输入送到字节数组解码入口,出现了以下结果。
六字节输入触发未受检解码异常
printf '\x0a\xff\xff\xff\xff\x07' > <PATH>/poc.bin && xxd <PATH>/poc.bin && echo "--- POSTING EXPLOIT ---" && curl -s --max-time 8 -w "\nHTTP %{http_code} exit=%{exitcode}\n" --data-binary @<PATH>/poc.bin http://<TARGET>:8080/decode; echo "curl done rc=$?"; sleep 1; echo "--- server process state ---"; ps aux | grep -v grep | grep WirePocServer || echo "PROCESS DEAD (good)"; echo "--- server.log ---"; cat <PATH>/server.log |
节选;占位符替换了实验专用值,未修改代码逻辑或结果。
异常中的 startIndex: 6 > endIndex: -2147483643 与整数回绕的推导一致,调用栈也经过 ByteArrayProtoReader32.readUtf8()、readString() 和 ProtoAdapter.decode()。HTTP 000 表示客户端没有收到 HTTP 响应,并非服务端返回了状态码 000;curl 的退出状态为 52。记录中的进程消失对应测试程序的 halt(1) 分支,库端直接证实的是未受检异常越过了解码接口。
修复、版本范围与排查
修复提交 25ebcab 调整了校验顺序:requireNonNegativeLength() 负责非负长度约束,checkedLimit(length) 则在更新边界之前确认字段确实放得下。以下是该提交对读取器的修改差异,关键是先检查剩余空间,再计算新的绝对位置。
修复在加法前检查剩余字节数
diff --git a/wire-runtime/src/commonMain/kotlin/com/squareup/wire/ByteArrayProtoReader32.kt b/wire-runtime/src/commonMain/kotlin/com/squareup/wire/ByteArrayProtoReader32.kt |
原始记录节选。
remainingInLimit() 先拒绝已经越过边界的读取位置,再返回 limit - pos。checkedLimit() 拒绝负字节数,以及任何大于剩余空间的字节数,并抛出受检的 EOFException;只有检查通过,才执行 pos + byteCount。这样,过大的正长度在加法前就被挡住,无法再靠回绕生成负边界。这段修复差异说明了校验逻辑,实际输入的处理结果则由下面的运行对照展示。
同一个 Java 测试程序加载 wire-runtime-jvm 6.4.5 后,接收了与前面相同的六字节字符串输入,并在之后通过 GET 检查能否继续响应。这里的版本依据是加载的运行库与类路径,而不是程序中仍写着 6.4.1 的固定横幅。
Wire 6.4.5 拒绝相同输入并保持可用
mkdir -p <PATH>/jars-fixed && cd <PATH> && cp jars/okio-jvm-3.17.0.jar jars/kotlin-stdlib-2.0.21.jar jars-fixed/ && cp wire-runtime-jvm-6.4.5.jar jars-fixed/ && nohup java -cp 'jars-fixed/*' WirePocServer.java 8077 > fixed.log 2>&1 & sleep 3; echo "--- trigger on 6.4.5 ---"; printf '\x0a\xff\xff\xff\xff\x07' | curl -s -m 5 -w '\nHTTP=%{http_code}\n' --data-binary @- http://<TARGET>:8077/decode; sleep 1; curl -s -o /dev/null -w 'alive-after GET=%{http_code}\n' -m 5 http://<TARGET>:8077/; tail -2 <PATH>/fixed.log |
节选;占位符替换了实验专用值,未修改代码逻辑或结果。
这次输入产生 EOFException,由测试程序转换成 HTTP 400,随后的 GET 返回 200。这表明同一条完整的字符串溢出输入已进入受检异常分支,程序处理后仍能响应;这一结果只覆盖该输入。
上游回归测试 ProtoReader32Test.lengthDelimitedRejectsPositiveLengthOverflow 为字符串、未知字段与 group 三种完整溢出输入都设置了抛出 IOException 的断言。这是测试源码表达的预期行为,不能等同于整套测试已经运行通过;直接运行对照也没有覆盖另外两种完整输入在 6.4.5 上的表现。修复还加固了其他 ProtoReader 读取路径和 Swift 的 ReadBuffer 缓冲区边界检查。
应用应将 wire-runtime 升级到对应发行线的修复版本:
| 发行线 | 声明的受影响范围 | 修复版本 |
|---|---|---|
| 稳定版 | < 6.4.5(即 ≤ 6.4.4) |
6.4.5 |
| 7.0.0 预发布版 | 7.0.0-alpha01 至 7.0.0-alpha03 |
7.0.0-alpha04 |
这里的直接运行时对照覆盖 JVM 6.4.1 与 6.4.5 的 ByteArray 解码入口。其他声明受影响版本、BufferedSource 流输入、直接使用 ProtoReader 的方式,以及 Swift 和 alpha 发行线的范围,依据的是公告及源码或修复差异,不能理解为每项都做过同样的运行测试。已有 Maven 版本列表在 6.4.1 后直接列出 6.4.5,未列出 6.4.2–6.4.4;这一列表现象不缩小声明的 < 6.4.5 范围。
部署排查应先确认实际加载的运行库版本,再检查哪些请求体、消息或其他外部字节会进入受影响的解码入口,以及这些入口是否受认证保护。还要检查解码失败的处理分支:只捕获 IOException 时,无法承接这里的未受检异常;应用是否会因此丢失请求、退出进程或反复重启,需要结合自身处理逻辑判断。这个判断用于评估实际影响,根本修复仍是使用已修复的运行库。
日志中值得关联查看的是:从 ProtoAdapter.decode() 逃出的 IllegalArgumentException,调用栈包含 ByteArrayProtoReader32.readString() 或 readUtf8(),并出现负的字符串结束索引;另一个线索是未知字段的 ByteString.of 处理或 group 跳过期间出现 ArrayIndexOutOfBoundsException,伴随异常字节数或负游标。这些现象应与畸形 protobuf 输入及请求、进程故障一起判断,单个异常名称不足以确定成因。仅看到 IOException 或 EOFException 拒绝输入,也不能认定已经发生此漏洞的可用性失败,因为修复版正是通过受检异常拒绝这类输入。