REDHAT-BUG-2536898: High severity Netty netty-codec-stomp vulnerability
ByteBuf Leak in StompSubframeDecoder When a Frame Body Is Never Terminated
A public GitHub Security Advisory (GHSA-ghg5-c4jg-8q5j) describes the following issue:
Summary
StompSubframeDecoder allocates a chunk buffer from the channel allocator once a frame's declared content-length has been satisfied, and parks it in an instance field while it waits for the single NUL byte that terminates the frame. If that byte never arrives, nothing ever releases the buffer. The decoder overrides neither handlerRemoved0 nor channelInactive, so the buffer survives the connection that created it.
A remote peer leaks one allocator buffer per connection by sending a complete, well-formed frame body and simply not sending its terminating byte. With the default pooled allocator the memory is never returned to the pool, so it is not reclaimed when the peer disconnects and not reclaimed by garbage collection. The leak accumulates for the lifetime of the process.
Details
In codec-stomp/src/main/java/io/netty/handler/codec/stomp/StompSubframeDecoder.java:
java case READCONTENT: ... if (contentLength >= 0) { int remainingLength = (int) (contentLength - alreadyReadChunkSize); if (toRead > remainingLength) { toRead = remainingLength; } ByteBuf chunkBuffer = readBytes(ctx.alloc(), in, toRead); // allocated if ((alreadyReadChunkSize += toRead) >= contentLength) { lastContent = new DefaultLastStompContentSubframe(chunkBuffer); // parked in a field checkpoint(State.FINALIZEFRAMEREAD); } ... // Fall through. case FINALIZEFRAMEREAD: skipNullCharacter(in); // needs one more byte ... out.add(lastContent); resetDecoder();
The class is a ReplayingDecoder. When the body is complete but the trailing NUL has not arrived, skipNullCharacter(in) throws a replay Signal so the decoder can be re-entered with more input. lastContent is left holding the allocated buffer, which is correct while the connection is alive and more data may still arrive.
The problem is what happens if more data never arrives. Three things combine:
Signal extends Error, not Exception. The catch (Exception e) block in decode(..) releases lastContent on failure, but a replay signal is not an Exception, so that path does not run.
resetDecoder() sets lastContent = null without releasing it, which is correct on the success path because ownership has already passed to out at that point, but it means the field is never a release site.
StompSubframeDecoder overrides neither handlerRemoved0 nor channelInactive. ByteToMessageDecoder.handlerRemoved releases the cumulation buffer, but knows nothing about this decoder's own field, so nothing releases lastContent at teardown.
The result is that between "body complete" and "NUL received" the decoder holds an allocator buffer with no release path other than the arrival of one specific byte that the peer controls.
Proof of concept
PocStompLeak.java drives the released io.netty:netty-codec-stomp:4.2.17.Final through EmbeddedChannel with a counting allocator that records every buffer the channel allocates, then reports which are still referenced after the channel is closed. Build and run with the attached pom.xml:
docker run --rm -v "$PWD:/work" -w /work maven:3-eclipse-temurin-21 mvn -q -B compile exec:java
Observed output:
=============== SINGLE CONNECTION ===============
CONTROL frame WITH trailing NUL buffers never freed : 0 bytes never freed : 0
ATTACK frame WITHOUT trailing NUL buffers never freed : 1 bytes never freed : 8,132
=============== SUSTAINED (500 CONNECTIONS) =============== connections : 500 buffers never freed : 500 bytes never freed : 4,066,000 (3.9 MiB) leaked per connection: 8,132 bytes
The control is the load-bearing part. It sends the byte-for-byte identical frame with its trailing NUL present, and leaks nothing. Because the only difference between the two runs is one byte, the leak is attributable to the missing terminator rather than to the harness or to how EmbeddedChannel is torn down.
The fix was verified the same way. Recompiling the decoder with the suggested patch and letting it shadow the released class, then re-running the identical PoC, gives 0 buffers and 0 bytes across all 500 connections.
Impact
A remote peer can permanently consume allocator memory in any application using netty's STOMP codec, at one buffer per connection, and can then disconnect. This is reachable before any application-level authentication, since a STOMP frame is sent immediately on connect.
Two properties matter more than raw volume here.
The memory is never reclaimed. This is not per-connection state that is released when the peer goes away, which is the usual shape of a resource-consumption issue; the buffer outlives the channel entirely. With PooledByteBufAllocator, netty's default, the underlying region is never returned to the pool, so neither disconnection nor garbage collection recovers it. Over a long-running process the leak is monotonic.
The attacker does not need to hold anything open. Because the leak survives the connection, an attacker can open, leak, and close in a tight loop, presenting a traffic pattern indistinguishable from clients that time out or crash mid-frame. There is no long-lived connection to spot and no rate-limit signature beyond ordinary connection churn.
We should be straightforward about what this is not. The leak is roughly one-to-one with bandwidth: leaking an 8,132 byte buffer requires sending 8,132 bytes, because the buffer is only parked once the declared content-length has actually been received. There is no amplification. The severity argument rests on permanence, not on ratio - normal traffic accumulates nothing, whereas this accumulates everything and never gives it back.
The body size is chosen by th
[truncated]
Affected: - maven:io.netty:netty-codec-stomp affected <= 4.1.137.Final; fixed unknown - maven:io.netty:netty-codec-stomp affected >= 4.2.0.Final, <= 4.2.17.Final; fixed unknown
Fixed versions: see advisory
Advisory: https://github.com/netty/netty/security/advisories/GHSA-ghg5-c4jg-8q5j
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
maven:io.netty:netty-codec-stompto a version that resolves this vulnerability.Fixed in 4.2.17.Final - Upgrade
Upgrade
maven:io.netty:netty-codec-stompto a version that resolves this vulnerability.Fixed in 4.1.137.Final - Compensating control
Use a network-level control to limit inbound STOMP connections (e.g., firewall/ACL/WAF/rate limiting on the STOMP port) to reduce the attack’s ability to rapidly create and terminate connections that trigger the allocator buffer leak.
Event History
Frequently Asked Questions
Who can exploit this issue?
A remote peer that can establish a connection to a service using StompSubframeDecoder can trigger the leak. The peer sends a complete, well-formed frame body with a declared content length, then omits the required terminating NUL byte.
What is the operational impact?
One allocator buffer is leaked per malicious connection. With the default pooled allocator, the memory is not returned to the pool on disconnect or reclaimed by garbage collection, so leaked memory accumulates for the lifetime of the process.
Does disconnecting the offending client recover the leaked memory?
No. The decoder retains the buffer after the connection that created it ends, and the leaked buffer is not reclaimed by garbage collection when the default pooled allocator is used.