Fedora Account System
Red Hat Associate
Red Hat Customer
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 `PP2_TYPE_SSL` 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 `PP2_TYPE_SSL` 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: AF_IPv4 + 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