A flaw was found in Netty. A reference-count leak in the HAProxy PROXY-v2 message decoder allows a remote, unauthenticated attacker to send specially crafted PROXY-protocol v2 headers. This can lead to memory exhaustion, resulting in a Denial of Service (DoS) for the affected system.
HAProxy PROXY-v2 nested-TLV grandchild ByteBuf reference-count leak (incomplete fix of PR #16881)
A public GitHub Security Advisory (GHSA-j58c-g352-8h4p) describes the following issue:
Summary PR #16881 introduced releaseDeep(...) (recursive release of a TLV tree) and converted the flattened-list call sites to release nested PP2TYPESSL TLVs correctly. However the inner catch inside readNextTLV (HAProxyMessage.java:340) still calls the flatten-unaware releaseTlvs(encapsulatedTlvs). There, encapsulatedTlvs is a non-flattened list of a single SSL TLV's direct children, where a child may itself be an HAProxySSLTLV holding grandchildren. releaseTlvs' skip-counter (HAProxyMessage.java:277) is designed only for the flattened top-level list; on this tree-shaped list it treats a child SSL TLV's grandchild-count as a skip over the following siblings and never releases the grandchildren. When a later sibling TLV is malformed and the inner catch fires, the retained grandchild slice (e.g. an ALPN TLV under a nested SSL TLV) is leaked, keeping the underlying header ByteBuf retained.
Reachability / trust boundary Remote, unauthenticated: HAProxyMessageDecoder (a ByteToMessageDecoder) parses PROXY-protocol v2 header bytes from the peer/upstream at the pipeline edge. The decoder documents no trusted-input assumption. Reference-count leaks while parsing nested PP2 TLVs from attacker-supplied bytes are an accepted Netty vulnerability class with a direct High precedent (GHSA-h2qv-fj59-j46j).
Impact Each crafted header leaks the grandchild slice, pinning the underlying pooled buffer. Sustained malformed headers accumulate leaked/pinned memory → memory-exhaustion DoS.
Honest caveat: demonstrated impact is exactly one pinned buffer per crafted connection (refCnt 2 vs 1); the DoS is reached by repetition/flooding (no per-message amplification). PROXY-protocol listeners are conventionally fronted by trusted upstream infrastructure, which narrows the realistic attacker population — but Netty as a library makes no such trust assumption.
Fix One-line change on the error path: use the existing releaseDeep(encapsulatedTlvs) instead of releaseTlvs(encapsulatedTlvs) in the readNextTLV inner catch. It runs only on the exception path, releases each tree node exactly once (the list is only ever populated via add, never addAll of an SSL child's list, so it is strictly tree-shaped — no double-free), and cannot affect valid PROXY traffic.
Proof of Concept Minimal 52-byte PROXY-v2 header: signature + ver/cmd=0x21 + TCP4 + 12 address/port bytes, then an outer PP2TYPESSL TLV containing client+verify and a nested SSL child holding a 1-byte ALPN grandchild, followed by a malformed sibling TLV that forces the inner catch.
java / Copyright 2024 The Netty Project The Netty Project licenses this file to you under the Apache License, version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. / package io.netty.handler.codec.haproxy;
import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue;
/ F001 - grandchild {@link ByteBuf} refCnt leak on the PROXY-v2 nested-SSL-TLV error path (module codec-haproxy, {@code HAProxyMessage.readNextTLV}). <p>When {@code readNextTLV} parses an SSL TLV it collects its immediate children into a NON-flattened list ({@code encapsulatedTlvs}). If a later sibling is malformed the inner catch releases that list with {@code releaseTlvs}, whose skip-counter assumes a FLATTENED list. A child that is itself an SSL TLV therefore has its grandchildren skipped and never released, leaking the grandchild's retained slice of the shared header buffer.</p> <p>Oracle: after the error-path decode throws, {@code header.refCnt()} is {@code 2} (grandchild slice still retained = leaked) on the vulnerable tree, and {@code 1} (grandchild released) once the inner catch releases recursively.</p> / public class HAProxyTLVGrandchildLeakF001Test {
/ A minimal (52-byte) PROXY-v2 header whose top-level SSL TLV encapsulates a child SSL TLV (holding one ALPN grandchild) followed by a malformed sibling SSL TLV that forces the error path. Every byte below is load-bearing; see the F001 evidence bundle. / private static byte[] malformedHeader() { return new byte[] { // -- 12-byte v2 signature (decodeHeader only skipBytes(12); contents unchecked) -- 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A, 0x21, // verCmd: version 2, PROXY command 0x11, // protFam: AFIPv4 + STREAM (TCP4) 0x00, 0x0C, // addressInfoLen = 12 (min for IPv4; does not bound the TLV region) 0x00, 0x00, 0x00, 0x00, // src addr 0.0.0.0 0x00, 0x00, 0x00, 0x00, // dst addr 0.0.0.0 0x00, 0x00, // src port 0x00, 0x00, // dst port // -- outer SSL TLV: type 0x20, len 21 -- 0x20, 0x00, 0x15, 0x00, // client 0x00, 0x00, 0x00, 0x00, // verify // ---- child SSL TLV: type 0x20, len 9 (a grandchild-holder) ---- 0x20, 0x00, 0x09, 0x00, // client 0x00, 0x00, 0x00, 0x00, // verify // ------ ALPN grandchild
[truncated]
Affected: - maven:io.netty:netty-codec-haproxy affected <= 4.2.17.Final; fixed unknown - maven:io.netty:netty-codec-haproxy affected <= 4.1.137.Final; fixed unknown
Fixed versions: see advisory
Advisory: https://github.com/netty/netty/security/advisories/GHSA-j58c-g352-8h4p