GHSA-93wv-jw9v-4972 is a high-severity (CVSS 7.5) Uncontrolled Resource Consumption vulnerability in io.netty:netty-codec-http2. O3 Security confirms whether GHSA-93wv-jw9v-4972 is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.
Netty: HTTP/2 decompression leaks ByteBuf reference count when the decompressor channel is already closed (Direct memory leak / OOM DoS)
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
- CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.
Exploitation and automatability from CISA’s SSVC triage for GHSA-93wv-jw9v-4972.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
How urgent is this, really
GHSA-93wv-jw9v-4972 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.
Where this sits among everything scored
Of 367,996 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.
Real-World Exposure
io.netty:netty-codec-http2☕io.netty:netty-codec-http2Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Maven packages — download data is not available via public APIs for these ecosystems.
Description
Summary
A remote, unauthenticated peer can leak one direct ByteBuf per HTTP/2 DATA frame in
applications that enable HTTP/2 content decompression via DelegatingDecompressorFrameListener.
When a DATA frame is processed for a stream whose decompressor has already been closed,
Http2Decompressor.decompress(...) retains the frame buffer but never releases it on the error
path, so its reference count never returns to zero. Repeating this over a long-lived HTTP/2
connection exhausts direct memory and crashes the JVM with OutOfMemoryError — a denial of service.
Details
In codec-http2/src/main/java/io/netty/handler/codec/http2/DelegatingDecompressorFrameListener.java,
Http2Decompressor.decompress(...) does:
// around line 433
decompressor.writeInbound(data.retain());
The argument data.retain() is evaluated before writeInbound(...) executes, incrementing the
buffer's reference count (refCnt: 1 -> 2). The very first statement of
EmbeddedChannel.writeInbound(...) is ensureOpen() (EmbeddedChannel.java:360), which throws
ClosedChannelException when the decompressor's internal EmbeddedChannel has already been closed.
When that happens:
- the
DATApayload has beenretain()ed but never entered the pipeline, so the decoder'sfinally { release() }never runs; - the surrounding
catch (Throwable t)block indecompress(...)(around line 451) does not release the extra reference; - the input buffer therefore can never reach refCnt 0, and its (typically direct) memory is leaked.
The decompressor channel is closed on a reachable path:
Http2Connection onStreamRemoved → Http2Decompressor.cleanup() →
EmbeddedChannel.finishAndReleaseAll()
(DelegatingDecompressorFrameListener.java:125-133 and 418-420).
A peer that sends DATA frames for a stream whose decompressor has already been cleaned up (e.g.
continuing to send DATA after END_STREAM / stream removal) thus leaks one direct ByteBuf per
frame.
Affected code: DelegatingDecompressorFrameListener.java, method Http2Decompressor.decompress(...)
— the decompressor.writeInbound(data.retain()) call (line ~433) and its catch (Throwable t)
block (line ~451), which lacks a data.release() rollback.
Suggested fix: track whether writeInbound succeeded and roll back the extra retain() only when
the data never entered the pipeline:
boolean writeSucceeded = false;
try {
decompressor.writeInbound(data.retain());
writeSucceeded = true; // pipeline now owns the release
if (endOfStream) {
decompressor.finish();
}
return 0;
} catch (Throwable t) {
if (!writeSucceeded) {
data.release(); // roll back the extra retain(); data never entered pipeline
}
if (t instanceof Http2Exception) {
throw (Http2Exception) t;
}
throw streamError(stream.id(), INTERNAL_ERROR, t, ...);
}
| Case | writeSucceeded | catch action | Reason |
|---|---|---|---|
ensureOpen() throws (this bug) | false | data.release() | data never entered pipeline |
| handler throws internally | true | no release | decoder finally already released |
finish() throws | true | no release | writeInbound already succeeded |
PoC
Reproduced against the official, unmodified netty-codec-http2-4.2.15.Final.jar from Maven Central,
using real netty classes and measuring ByteBuf.refCnt() directly (the leaking logic is not mocked).
Reproduction steps:
- Download the official artifacts and their dependencies from Maven Central (version
4.2.15.Final):netty-common,netty-buffer,netty-transport,netty-resolver,netty-handler,netty-codec-base,netty-codec,netty-codec-http,netty-codec-http2,netty-codec-compression. - Build a real
Http2Decompressorwrapping a real gzip decoderEmbeddedChannel(ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP)). - Close the internal decompressor channel (equivalent to the end state of
cleanup()/finishAndReleaseAll()). - Encode a real gzip
DATApayload withZlibCodecFactory.newZlibEncoder(GZIP)(refCnt = 1). - Call
decompress(...)on the closed channel. - Observe:
writeInbound(...)throwsClosedChannelExceptionat itsensureOpen()entry (EmbeddedChannel.java:360), reached fromDelegatingDecompressorFrameListener.java:433;data.refCnt()is now2. - Release once as the frame reader would;
refCntstays at1(release()returnsfalse) → leaked.
Observed reference-count trace:
gzipData initial refCnt = 1
decompress -> data.retain() -> refCnt = 2 (retain applied, never rolled back)
caller releases once -> refCnt = 1 (release() returns false; not deallocated)
=> buffer never reaches 0 -> direct memory leaked
Observed exception stack (confirms the leak point):
java.nio.channels.ClosedChannelException
at io.netty.channel.embedded.EmbeddedChannel.checkOpen(EmbeddedChannel.java:959)
at io.netty.channel.embedded.EmbeddedChannel.ensureOpen(EmbeddedChannel.java:979)
at io.netty.channel.embedded.EmbeddedChannel.writeInbound(EmbeddedChannel.java:360)
at io.netty.handler.codec.http2.DelegatingDecompressorFrameListener$Http2Decompressor
.decompress(DelegatingDecompressorFrameListener.java:433)
Two notes on the harness (they do not affect the leak mechanism):
- The internal channel is closed directly via
close()rather than throughcleanup(). The end state is identical (channel closed →writeInboundthrows atensureOpen()); the bug depends on "channel closed → retain not rolled back", not on how the channel was closed. - In the isolated harness the rethrown
StreamException's root cause shows asNullPointerExceptionbecause the harness does not initialise anHttp2LocalFlowController(a secondary exception reported during channel close). The leak is already sealed at theClosedChannelExceptionthrown bywriteInbound'sensureOpen()(line 360); in a real server with the flow controller initialised, the triggering exception is theClosedChannelExceptionitself.
A complete self-contained PoC (Verify02DecompressLeak.java, ~150 lines, no test framework) plus the
exact javac / java commands can be attached on request.
Impact
- Vulnerability type: uncontrolled resource consumption / memory leak (CWE-401), leading to
denial of service. Each crafted
DATAframe leaks one (typically direct/off-heap)ByteBuf. - Who is impacted: any server (or client) that enables HTTP/2 content decompression by installing
DelegatingDecompressorFrameListenerin its HTTP/2 pipeline. - Attacker requirements: remote, unauthenticated. The attacker only needs to send HTTP/2
DATAframes for a stream whose decompressor has been cleaned up (e.g. continue sendingDATAafterEND_STREAM). No special server configuration beyond decompression being enabled. - Result: sustained triggering over a long-lived connection exhausts direct memory and crashes
the JVM with
OutOfMemoryError.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| ☕Maven | io.netty:netty-codec-http2 | ≥ 4.2.0&&< 4.2.16.Final | 4.2.16.Final |
| ☕Maven | io.netty:netty-codec-http2 | ≥ 4.1.0.Final&&< 4.1.136.Final | 4.1.136.Final |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for io.netty:netty-codec-http2. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update io.netty:netty-codec-http2 to 4.2.16.Final or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-93wv-jw9v-4972 is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-93wv-jw9v-4972 is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-93wv-jw9v-4972. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Fixing This On Your OS
If you run this on a Linux distribution, patch through your package manager against the distro's own security advisory below — it tracks the exact backported fix for your release, which can ship on a different timeline (and sometimes a different severity) than the upstream project.
This is an Important vulnerability in Netty's HTTP/2 codec, which could lead to a denial of service. Red Hat products utilizing netty-codec-http2 with HTTP/2 content decompression enabled are susceptible to memory exhaustion when processing specially crafted HTTP/2 DATA frames. This allows a remote attacker to leak…
Frequently Asked Questions
Is GHSA-93wv-jw9v-4972 in your dependencies?
O3 detects GHSA-93wv-jw9v-4972 across Maven dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.