Proxy Protocol v2 Header Length Undercount Enables Smuggling
Affected commit: c33d01624d
Sink: source/extensions/common/proxy_protocol/proxy_protocol_header.cc:230
Summary
With proxy_protocol_remove_too_long_tlvs disabled, generateV2Header() calculates the advertised frame length from only the fitting TLVs but serializes all TLVs including over-limit ones. Extra bytes after the declared frame boundary are parsed by the receiver as the start of the next protocol message.
Reproduce
#!/bin/bash
set -e
# Clone and identify HEAD
git clone --depth 1 https://github.com/envoyproxy/envoy.git /tmp/envoy-repro
cd /tmp/envoy-repro
echo "HEAD: $(git rev-parse HEAD)"
# Verify length is computed from fitting TLVs only
echo "--- extension_length accumulates only fitting TLVs ---"
sed -n '215,225p' source/extensions/common/proxy_protocol/proxy_protocol_header.cc
# Verify serialization uses combined_tlv_vector when remove_too_long_tlvs is false
echo "--- Serialization uses all TLVs when not removing ---"
sed -n '227,237p' source/extensions/common/proxy_protocol/proxy_protocol_header.cc
The vulnerable code at proxy_protocol_header.cc:215-237:
// Phase 1: compute extension_length from only fitting TLVs
for (auto&& tlv : combined_tlv_vector) {
uint64_t new_size = extension_length + PROXY_PROTO_V2_TLV_TYPE_LENGTH_LEN + tlv.value.size();
if (new_size > max_extension_length) {
skipped_tlvs = true;
continue; // skipped in length calculation
}
extension_length = new_size;
final_tlvs.push_back(tlv);
}
// Phase 2: write header with extension_length (undercounted)
generateV2Header(..., static_cast<uint16_t>(extension_length), out);
// Phase 3: serialize ALL TLVs when remove_too_long_tlvs is false
const std::vector<...>& really_final_tlvs =
remove_too_long_tlvs ? final_tlvs : combined_tlv_vector;
for (auto&& tlv : really_final_tlvs) {
out.add(&tlv.type, 1);
...
}
The proxy protocol v2 header advertises extension_length computed from only the fitting TLVs, but when remove_too_long_tlvs is false, the serialization loop writes combined_tlv_vector — including the over-limit TLVs. The receiver reads extension_length bytes as TLV data, then interprets the remaining over-limit TLV bytes as the start of the next protocol message. This enables request smuggling or parser confusion on the upstream receiver.
Expected output:
HEAD: <resolved-commit>
--- extension_length accumulates only fitting TLVs ---
for (auto&& tlv : combined_tlv_vector) {
uint64_t new_size = extension_length + PROXY_PROTO_V2_TLV_TYPE_LENGTH_LEN + tlv.value.size();
if (new_size > max_extension_length) {
ENVOY_LOG_MISC(warn, "Skipping TLV type {} because adding it would exceed the 65535 limit.",
tlv.type);
skipped_tlvs = true;
continue;
}
extension_length = new_size;
final_tlvs.push_back(tlv);
}
--- Serialization uses all TLVs when not removing ---
generateV2Header(src.addressAsString(), dst.addressAsString(), src.port(), dst.port(),
src.version(), static_cast<uint16_t>(extension_length), out);
const std::vector<Envoy::Network::ProxyProtocolTLV>& really_final_tlvs =
remove_too_long_tlvs ? final_tlvs : combined_tlv_vector;
for (auto&& tlv : really_final_tlvs) {
out.add(&tlv.type, 1);
uint16_t size = htons(static_cast<uint16_t>(tlv.value.size()));
out.add(&size, sizeof(uint16_t));
out.add(&tlv.value.front(), tlv.value.size());
}
Credit
Zheng Yu @ Depthfirst