Envoy thrift_proxy 宽松二进制协议 32 位整数溢出漏洞:成因、修复与回归测试解析
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
本文基于 Envoy 当前版本变更记录 中的 bug fix 条目展开,深入解析
thrift_proxy过滤器在解析 Thrift 宽松(non-strict/lax)二进制协议时暴露的 32 位整数溢出缺陷:一条name_len字段大于等于0xFFFFFFF7的消息即可绕过"数据不足"检查,触发虚假解码错误并关闭下游连接。文章将结合协议格式、解码器源码与单元测试,完整还原漏洞触发链路、64 位算术修复方案及其对线上升级的影响。读完本文,你将掌握 Thrift 二进制协议两种编码风格的差异、readMessageBegin的解析流程,以及如何从源码与测试层面验证此类整数回绕问题的修复正确性。
背景:Thrift 二进制协议的严格与宽松两种编码风格
Envoy 的thrift_proxy网络过滤器用于将 Thrift RPC 流量接入服务网格(边缘/中间/服务代理场景)。它支持的协议族中,二进制协议(Binary Protocol)存在两种编码变体,分别由 binary_protocol_impl.h 中的两个类实现:
| 实现类 | 协议名 | 特点 |
|---|---|---|
BinaryProtocolImpl | binary(严格协议) | 消息头包含固定的 2 字节魔数0x8001版本字段 |
LaxBinaryProtocolImpl | binary/non-strict(宽松协议) | 省略版本魔数,直接以 4 字节方法名长度开头 |
在 thrift_proxy.proto 的ProtocolType枚举中,两者分别对应BINARY = 1与LAX_BINARY = 2。需要注意的一个关键约束是:宽松二进制协议不参与自动协议检测——即使用AUTO模式时,Envoy 只会尝试识别严格二进制、compact 与 Twitter 协议,宽松二进制必须通过protocol字段显式指定。
两种协议的**消息头(Message Begin)**布局差异如下:
严格协议(BinaryProtocolImpl,最小 12 字节): +--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+ | version(2B, 0x8001) | unused(1B) | type(1B) | name_len(4B) | name(0+ B) | seq_id(4B) | +---------------------+------------+----------+-----------------+----------------------+--------------+ 宽松协议(LaxBinaryProtocolImpl,最小 9 字节): +-----------------+-----------------+----------+--------------+ | name_len(4B) | name(0+ B) | type(1B) | seq_id(4B) | +-----------------+-----------------+----------+--------------+这一布局差异在 binary_protocol_impl.cc 的LaxBinaryProtocolImpl::readMessageBegin注释中有明确记录,也是本次溢出漏洞的温床。
漏洞详情:readMessageBegin中的 32 位整数回绕
漏洞触发条件
本次修复的 bug fix 条目明确指出:
Fixed a 32-bit integer overflow in the
thrift_proxylax (non-strict) binary protocol decoder. A message name length of0xFFFFFFF7or greater wrapped the insufficient-data check inreadMessageBeginand raised a spurious decode error that closed the downstream connection.
即:当恶意或畸形请求携带的方法名长度name_len >= 0xFFFFFFF7时,解码器会出现问题。0xFFFFFFF7 = 4,294,967,287,恰好是2^32 - 9。
根因分析:name_len + 9的隐式 32 位运算
修复前的宽松协议readMessageBegin中,"数据不足"检查形如:
uint32_t name_len = buffer.peekBEInt<uint32_t>(); // 4 字节大端读取 if (buffer.length() < name_len + 9) { // BUG: 32 位算术回绕 return false; }问题出在name_len + 9:name_len是uint32_t,而9是int字面量,两者相加时按32 位整数运算执行。当name_len足够大时,加法结果会回绕(wrap around):
name_len = 0xFFFFFFF7时:0xFFFFFFF7 + 9 = 0x100000000,截断为 32 位后得到0;name_len = 0xFFFFFFFF时:0xFFFFFFFF + 9 = 0x100000008,截断为 32 位后得到8。
此时若缓冲区中已有 9 字节(最小消息头长度),形如buffer.length() < 0或buffer.length() < 8的比较结果为false,"数据不足"检查被成功绕过,代码继续向下执行。
越界访问与虚假解码错误
绕过数据检查后,解码器会继续执行方法名字符串的读取,以及消息类型、序列号的解析:
MessageType type = static_cast<MessageType>(buffer.peekInt<int8_t>(name_len + 4));这里name_len + 4同样是 32 位运算:0xFFFFFFF7 + 4 = 0xFFFFFFFB,这个回绕后的偏移量指向缓冲区外的非法区域,peekInt直接抛出EnvoyException(如invalid (lax) binary protocol message type或缓冲区越界异常),从而产生虚假的解码错误。
从调用链看,Decoder::onData在 decoder.cc 中调用proto_.readMessageBegin(buffer, *metadata_),正常情况下返回false表示"数据不足,等待更多数据",状态机进入ProtocolState::WaitForData;而本次缺陷中异常被抛出,连接管理器将其视为协议错误,最终关闭下游连接。这意味着攻击者无需发送完整消息,仅需 9 字节精心构造的头部即可触发一次连接中断,可用于低成本地发起连接耗尽类攻击(尽管无内存破坏风险)。
修复方案:检查改用 64 位算术,与严格协议保持一致
修复后的LaxBinaryProtocolImpl::readMessageBegin(binary_protocol_impl.cc)如下:
uint32_t name_len = buffer.peekBEInt<uint32_t>(); if (buffer.length() < static_cast<uint64_t>(name_len) + 9) { return false; }关键变化仅有一处:将name_len显式提升为uint64_t后再与9相加。这样:
0xFFFFFFF7 + 9 = 4,294,967,296(64 位下不截断),buffer.length() = 9 < 4,294,967,296恒成立,返回false;- 解码器状态机回到
ProtocolState::WaitForData,等待后续数据到达,而不是抛出异常关闭连接。
值得一提的是,严格二进制协议从未受此缺陷影响:在BinaryProtocolImpl::readMessageBegin中(binary_protocol_impl.cc),MinMessageBeginLength被声明为static constexpr uint64_t = 12(见 binary_protocol_impl.h),因此name_len + MinMessageBeginLength中name_len会被隐式提升为uint64_t再进行加法,天然具备 64 位精度。本次修复正是让宽松协议在语义上与严格协议对齐——正如 bug fix 条目所述 "matching the strict binary protocol"。
从防御性编程角度看,该修复思路值得在同类协议解码器中推广:凡涉及"长度字段 + 固定头部长度"与缓冲区长度比较的代码,都应确保以 64 位(或至少不小于缓冲区长度类型的宽度)进行算术运算,防止恶意长度字段通过整数回绕绕过边界检查。
回归测试:覆盖两种回绕形态
本次修复随附的单元测试位于 binary_protocol_impl_test.cc 的LaxBinaryProtocolTest.ReadMessageBegin中,新增了两个针对性用例:
用例一:长度回绕使"数据不足"检查失效
// Name length that wraps 32-bit arithmetic in the insufficient-data check buffer.writeBEInt<uint32_t>(0xFFFFFFF7); // name length addRepeated(buffer, 5, 'x'); bool result = true; EXPECT_NO_THROW(result = proto.readMessageBegin(buffer, metadata_)); EXPECT_FALSE(result); expectDefaultMetadata(); EXPECT_EQ(buffer.length(), 9);构造name_len = 0xFFFFFFF7(旧代码下+9回绕为 0),断言:不抛出异常、返回false(等待更多数据)、元数据保持默认值、缓冲区 9 字节原样保留。
用例二:回绕后的 peek 偏移落入长度字段内部
// Name length whose wrapped peek offset lands inside the length field itself buffer.writeBEInt<uint32_t>(0xFFFFFFFF); // name length addRepeated(buffer, 5, 'x'); bool result = true; EXPECT_NO_THROW(result = proto.readMessageBegin(buffer, metadata_)); EXPECT_FALSE(result); expectDefaultMetadata(); EXPECT_EQ(buffer.length(), 9);构造name_len = 0xFFFFFFFF(旧代码下+9回绕为 8,name_len + 4的 peek 偏移回绕为 3、落在长度字段内部),同样断言无异常、返回false。
两个用例共同验证了修复后的行为:任何name_len >= 0xFFFFFFF7的畸形头部都不再产生虚假解码错误,而是被当作"数据未齐"处理,等待更多输入。这正是非严格协议解码器应当遵循的安全语义。
影响评估与升级建议
- 影响面:仅影响显式配置
LAX_BINARY协议(或上游ThriftProtocolOptions指定 lax binary)的 Envoy 实例;使用严格二进制、compact、Twitter 协议或AUTO自动检测的流量不受影响。 - 攻击向量:攻击者无需完整 RPC 消息,仅需发送 9 字节最小头部(其中 4 字节为
name_len >= 0xFFFFFFF7)即可触发一次连接关闭,可被用于反复建立连接造成资源消耗;不涉及内存破坏或代码执行,属于健壮性/可用性类缺陷。 - 升级动作:升级到包含该修复的 Envoy 版本即可消除漏洞。若无法立即升级,可在网关/入口处对 Thrift 流量实施深度报文检查,拦截头部
name_len异常的请求,作为临时缓解手段。
小结
本次thrift_proxy宽松二进制协议漏洞是一例教科书式的32 位整数回绕绕过边界检查问题:一个uint32_t长度字段与整型字面量的加法在 32 位宽度下溢出,使"数据不足"检查形同虚设,进而由越界访问引发虚假解码错误并中断下游连接。修复方案(显式 64 位算术)简洁且与严格协议实现对齐,配套的单元测试精确覆盖了0xFFFFFFF7与0xFFFFFFFF两种回绕形态,为后续回归提供了坚实保障。对协议解码器开发者而言,本案例是"长度计算必须使用与缓冲区长度等宽或更宽的算术类型"这一原则的生动注脚。
关联参考:漏洞说明见 bug_fixes/thrift_proxy__lax-binary-message-length-overflow.rst;解码实现见 binary_protocol_impl.cc;协议类声明见 binary_protocol_impl.h;回归测试见 binary_protocol_impl_test.cc;解码状态机调用见 decoder.cc;协议类型配置见 thrift_proxy.proto。
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考