Netty MadeYouReset HTTP/2 DDoS Vulnerability
Description
Netty is an asynchronous, event-driven network application framework. Prior to versions 4.1.124.Final and 4.2.4.Final, Netty is vulnerable to MadeYouReset DDoS. This is a logical vulnerability in the HTTP/2 protocol, that uses malformed HTTP/2 control frames in order to break the max concurrent streams limit - which results in resource exhaustion and distributed denial of service. This issue has been patched in versions 4.1.124.Final and 4.2.4.Final.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
io.netty:netty-codec-http2Maven | >= 4.2.0.Alpha1, < 4.2.4.Final | 4.2.4.Final |
io.netty:netty-codec-http2Maven | < 4.1.124.Final | 4.1.124.Final |
io.grpc:grpc-netty-shadedMaven | < 1.75.0 | 1.75.0 |
Affected products
1Patches
26462ef9a1198netty: Count sent RST_STREAMs against limit
2 files changed · +135 −35
netty/src/main/java/io/grpc/netty/NettyServerHandler.java+91 −35 modified@@ -60,6 +60,7 @@ import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http2.DecoratingHttp2ConnectionEncoder; import io.netty.handler.codec.http2.DecoratingHttp2FrameWriter; import io.netty.handler.codec.http2.DefaultHttp2Connection; import io.netty.handler.codec.http2.DefaultHttp2ConnectionDecoder; @@ -83,6 +84,7 @@ import io.netty.handler.codec.http2.Http2Headers; import io.netty.handler.codec.http2.Http2HeadersDecoder; import io.netty.handler.codec.http2.Http2InboundFrameLogger; +import io.netty.handler.codec.http2.Http2LifecycleManager; import io.netty.handler.codec.http2.Http2OutboundFrameLogger; import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.codec.http2.Http2Stream; @@ -125,13 +127,11 @@ class NettyServerHandler extends AbstractNettyHandler { private final long keepAliveTimeoutInNanos; private final long maxConnectionAgeInNanos; private final long maxConnectionAgeGraceInNanos; - private final int maxRstCount; - private final long maxRstPeriodNanos; + private final RstStreamCounter rstStreamCounter; private final List<? extends ServerStreamTracer.Factory> streamTracerFactories; private final TransportTracer transportTracer; private final KeepAliveEnforcer keepAliveEnforcer; private final Attributes eagAttributes; - private final Ticker ticker; /** Incomplete attributes produced by negotiator. */ private Attributes negotiationAttributes; private InternalChannelz.Security securityInfo; @@ -149,8 +149,6 @@ class NettyServerHandler extends AbstractNettyHandler { private ScheduledFuture<?> maxConnectionAgeMonitor; @CheckForNull private GracefulShutdown gracefulShutdown; - private int rstCount; - private long lastRstNanoTime; static NettyServerHandler newHandler( ServerTransportListener transportListener, @@ -251,13 +249,20 @@ static NettyServerHandler newHandler( final KeepAliveEnforcer keepAliveEnforcer = new KeepAliveEnforcer( permitKeepAliveWithoutCalls, permitKeepAliveTimeInNanos, TimeUnit.NANOSECONDS); + if (ticker == null) { + ticker = Ticker.systemTicker(); + } + + RstStreamCounter rstStreamCounter + = new RstStreamCounter(maxRstCount, maxRstPeriodNanos, ticker); // Create the local flow controller configured to auto-refill the connection window. connection.local().flowController( new DefaultHttp2LocalFlowController(connection, DEFAULT_WINDOW_UPDATE_RATIO, true)); frameWriter = new WriteMonitoringFrameWriter(frameWriter, keepAliveEnforcer); Http2ConnectionEncoder encoder = new DefaultHttp2ConnectionEncoder(connection, frameWriter); encoder = new Http2ControlFrameLimitEncoder(encoder, 10000); + encoder = new Http2RstCounterEncoder(encoder, rstStreamCounter); Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, frameReader); @@ -266,10 +271,6 @@ static NettyServerHandler newHandler( settings.maxConcurrentStreams(maxStreams); settings.maxHeaderListSize(maxHeaderListSize); - if (ticker == null) { - ticker = Ticker.systemTicker(); - } - return new NettyServerHandler( channelUnused, connection, @@ -286,8 +287,7 @@ static NettyServerHandler newHandler( maxConnectionAgeInNanos, maxConnectionAgeGraceInNanos, keepAliveEnforcer, autoFlowControl, - maxRstCount, - maxRstPeriodNanos, + rstStreamCounter, eagAttributes, ticker); } @@ -310,8 +310,7 @@ private NettyServerHandler( long maxConnectionAgeGraceInNanos, final KeepAliveEnforcer keepAliveEnforcer, boolean autoFlowControl, - int maxRstCount, - long maxRstPeriodNanos, + RstStreamCounter rstStreamCounter, Attributes eagAttributes, Ticker ticker) { super( @@ -363,12 +362,9 @@ public void onStreamClosed(Http2Stream stream) { this.maxConnectionAgeInNanos = maxConnectionAgeInNanos; this.maxConnectionAgeGraceInNanos = maxConnectionAgeGraceInNanos; this.keepAliveEnforcer = checkNotNull(keepAliveEnforcer, "keepAliveEnforcer"); - this.maxRstCount = maxRstCount; - this.maxRstPeriodNanos = maxRstPeriodNanos; + this.rstStreamCounter = rstStreamCounter; this.eagAttributes = checkNotNull(eagAttributes, "eagAttributes"); - this.ticker = checkNotNull(ticker, "ticker"); - this.lastRstNanoTime = ticker.read(); streamKey = encoder.connection().newKey(); this.transportListener = checkNotNull(transportListener, "transportListener"); this.streamTracerFactories = checkNotNull(streamTracerFactories, "streamTracerFactories"); @@ -575,24 +571,9 @@ private void onDataRead(int streamId, ByteBuf data, int padding, boolean endOfSt } private void onRstStreamRead(int streamId, long errorCode) throws Http2Exception { - if (maxRstCount > 0) { - long now = ticker.read(); - if (now - lastRstNanoTime > maxRstPeriodNanos) { - lastRstNanoTime = now; - rstCount = 1; - } else { - rstCount++; - if (rstCount > maxRstCount) { - throw new Http2Exception(Http2Error.ENHANCE_YOUR_CALM, "too_many_rststreams") { - @SuppressWarnings("UnsynchronizedOverridesSynchronized") // No memory accesses - @Override - public Throwable fillInStackTrace() { - // Avoid the CPU cycles, since the resets may be a CPU consumption attack - return this; - } - }; - } - } + Http2Exception tooManyRstStream = rstStreamCounter.countRstStream(); + if (tooManyRstStream != null) { + throw tooManyRstStream; } try { @@ -1180,6 +1161,81 @@ public ChannelFuture writeHeaders(ChannelHandlerContext ctx, int streamId, Http2 } } + private static final class Http2RstCounterEncoder extends DecoratingHttp2ConnectionEncoder { + private final RstStreamCounter rstStreamCounter; + private Http2LifecycleManager lifecycleManager; + + Http2RstCounterEncoder(Http2ConnectionEncoder encoder, RstStreamCounter rstStreamCounter) { + super(encoder); + this.rstStreamCounter = rstStreamCounter; + } + + @Override + public void lifecycleManager(Http2LifecycleManager lifecycleManager) { + this.lifecycleManager = lifecycleManager; + super.lifecycleManager(lifecycleManager); + } + + @Override + public ChannelFuture writeRstStream( + ChannelHandlerContext ctx, int streamId, long errorCode, ChannelPromise promise) { + ChannelFuture future = super.writeRstStream(ctx, streamId, errorCode, promise); + // We want to count "induced" RST_STREAM, where the server sent a reset because of a malformed + // frame. + boolean normalRst + = errorCode == Http2Error.NO_ERROR.code() || errorCode == Http2Error.CANCEL.code(); + if (!normalRst) { + Http2Exception tooManyRstStream = rstStreamCounter.countRstStream(); + if (tooManyRstStream != null) { + lifecycleManager.onError(ctx, true, tooManyRstStream); + ctx.close(); + } + } + return future; + } + } + + private static final class RstStreamCounter { + private final int maxRstCount; + private final long maxRstPeriodNanos; + private final Ticker ticker; + private int rstCount; + private long lastRstNanoTime; + + RstStreamCounter(int maxRstCount, long maxRstPeriodNanos, Ticker ticker) { + checkArgument(maxRstCount >= 0, "maxRstCount must be non-negative: %s", maxRstCount); + this.maxRstCount = maxRstCount; + this.maxRstPeriodNanos = maxRstPeriodNanos; + this.ticker = checkNotNull(ticker, "ticker"); + this.lastRstNanoTime = ticker.read(); + } + + /** Returns non-{@code null} when the connection should be killed by the caller. */ + private Http2Exception countRstStream() { + if (maxRstCount == 0) { + return null; + } + long now = ticker.read(); + if (now - lastRstNanoTime > maxRstPeriodNanos) { + lastRstNanoTime = now; + rstCount = 1; + } else { + rstCount++; + if (rstCount > maxRstCount) { + return new Http2Exception(Http2Error.ENHANCE_YOUR_CALM, "too_many_rststreams") { + @SuppressWarnings("UnsynchronizedOverridesSynchronized") // No memory accesses + @Override + public Throwable fillInStackTrace() { + // Avoid the CPU cycles, since the resets may be a CPU consumption attack + return this; + } + }; + } + } + return null; + } + } + private static class ServerChannelLogger extends ChannelLogger { private static final Logger log = Logger.getLogger(ChannelLogger.class.getName());
netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java+44 −0 modified@@ -1304,6 +1304,8 @@ public void maxRstCount_exceedsLimit_fails() throws Exception { } private void rapidReset(int burstSize) throws Exception { + when(streamTracerFactory.newServerStreamTracer(anyString(), any(Metadata.class))) + .thenAnswer((args) -> new TestServerStreamTracer()); Http2Headers headers = new DefaultHttp2Headers() .method(HTTP_METHOD) .set(CONTENT_TYPE_HEADER, new AsciiString("application/grpc", UTF_8)) @@ -1323,6 +1325,48 @@ private void rapidReset(int burstSize) throws Exception { } } + @Test + public void maxRstCountSent_withinLimit_succeeds() throws Exception { + maxRstCount = 10; + maxRstPeriodNanos = TimeUnit.MILLISECONDS.toNanos(100); + manualSetUp(); + madeYouReset(maxRstCount); + + assertTrue(channel().isOpen()); + } + + @Test + public void maxRstCountSent_exceedsLimit_fails() throws Exception { + maxRstCount = 10; + maxRstPeriodNanos = TimeUnit.MILLISECONDS.toNanos(100); + manualSetUp(); + assertThrows(ClosedChannelException.class, () -> madeYouReset(maxRstCount + 1)); + + assertFalse(channel().isOpen()); + } + + private void madeYouReset(int burstSize) throws Exception { + when(streamTracerFactory.newServerStreamTracer(anyString(), any(Metadata.class))) + .thenAnswer((args) -> new TestServerStreamTracer()); + Http2Headers headers = new DefaultHttp2Headers() + .method(HTTP_METHOD) + .set(CONTENT_TYPE_HEADER, new AsciiString("application/grpc", UTF_8)) + .set(TE_HEADER, TE_TRAILERS) + .path(new AsciiString("/foo/bar")); + int streamId = 1; + long rpcTimeNanos = maxRstPeriodNanos / 2 / burstSize; + for (int period = 0; period < 3; period++) { + for (int i = 0; i < burstSize; i++) { + channelRead(headersFrame(streamId, headers)); + channelRead(windowUpdate(streamId, 0)); + streamId += 2; + fakeClock().forwardNanos(rpcTimeNanos); + } + while (channel().readOutbound() != null) {} + fakeClock().forwardNanos(maxRstPeriodNanos - rpcTimeNanos * burstSize + 1); + } + } + private void createStream() throws Exception { Http2Headers headers = new DefaultHttp2Headers() .method(HTTP_METHOD)
be53dc3c9acdHTTP2: Http2ConnectionHandler should always use Http2ConnectionEncode… (#15518)
2 files changed · +13 −13
codec-http2/src/main/java/io/netty/handler/codec/http2/Http2ConnectionHandler.java+3 −3 modified@@ -717,7 +717,7 @@ protected void onStreamError(ChannelHandlerContext ctx, boolean outbound, try { stream = encoder.connection().remote().createStream(streamId, true); } catch (Http2Exception e) { - resetUnknownStream(ctx, streamId, http2Ex.error().code(), ctx.newPromise()); + encoder().writeRstStream(ctx, streamId, http2Ex.error().code(), ctx.newPromise()); return; } } @@ -734,10 +734,10 @@ protected void onStreamError(ChannelHandlerContext ctx, boolean outbound, if (stream == null) { if (!outbound || connection().local().mayHaveCreatedStream(streamId)) { - resetUnknownStream(ctx, streamId, http2Ex.error().code(), ctx.newPromise()); + encoder().writeRstStream(ctx, streamId, http2Ex.error().code(), ctx.newPromise()); } } else { - resetStream(ctx, stream, http2Ex.error().code(), ctx.newPromise()); + encoder().writeRstStream(ctx, streamId, http2Ex.error().code(), ctx.newPromise()); } }
codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionHandlerTest.java+10 −10 modified@@ -421,7 +421,7 @@ public void serverShouldSend431OnHeaderSizeErrorWhenDecodingInitialHeaders() thr when(connection.isServer()).thenReturn(true); when(stream.isHeadersSent()).thenReturn(false); when(remote.lastStreamCreated()).thenReturn(STREAM_ID); - when(frameWriter.writeRstStream(eq(ctx), eq(STREAM_ID), + when(encoder.writeRstStream(eq(ctx), eq(STREAM_ID), eq(PROTOCOL_ERROR.code()), eq(promise))).thenReturn(future); handler.exceptionCaught(ctx, e); @@ -431,7 +431,7 @@ public void serverShouldSend431OnHeaderSizeErrorWhenDecodingInitialHeaders() thr captor.capture(), eq(padding), eq(true), eq(promise)); Http2Headers headers = captor.getValue(); assertEquals(HttpResponseStatus.REQUEST_HEADER_FIELDS_TOO_LARGE.codeAsText(), headers.status()); - verify(frameWriter).writeRstStream(ctx, STREAM_ID, PROTOCOL_ERROR.code(), promise); + verify(encoder).writeRstStream(ctx, STREAM_ID, PROTOCOL_ERROR.code(), promise); } @Test @@ -445,14 +445,14 @@ public void serverShouldNeverSend431HeaderSizeErrorWhenEncoding() throws Excepti when(connection.isServer()).thenReturn(true); when(stream.isHeadersSent()).thenReturn(false); when(remote.lastStreamCreated()).thenReturn(STREAM_ID); - when(frameWriter.writeRstStream(eq(ctx), eq(STREAM_ID), + when(encoder.writeRstStream(eq(ctx), eq(STREAM_ID), eq(PROTOCOL_ERROR.code()), eq(promise))).thenReturn(future); handler.exceptionCaught(ctx, e); verify(encoder, never()).writeHeaders(eq(ctx), eq(STREAM_ID), any(Http2Headers.class), eq(padding), eq(true), eq(promise)); - verify(frameWriter).writeRstStream(ctx, STREAM_ID, PROTOCOL_ERROR.code(), promise); + verify(encoder).writeRstStream(ctx, STREAM_ID, PROTOCOL_ERROR.code(), promise); } @Test @@ -466,14 +466,14 @@ public void clientShouldNeverSend431WhenHeadersAreTooLarge() throws Exception { when(connection.isServer()).thenReturn(false); when(stream.isHeadersSent()).thenReturn(false); when(remote.lastStreamCreated()).thenReturn(STREAM_ID); - when(frameWriter.writeRstStream(eq(ctx), eq(STREAM_ID), + when(encoder.writeRstStream(eq(ctx), eq(STREAM_ID), eq(PROTOCOL_ERROR.code()), eq(promise))).thenReturn(future); handler.exceptionCaught(ctx, e); verify(encoder, never()).writeHeaders(eq(ctx), eq(STREAM_ID), any(Http2Headers.class), eq(padding), eq(true), eq(promise)); - verify(frameWriter).writeRstStream(ctx, STREAM_ID, PROTOCOL_ERROR.code(), promise); + verify(encoder).writeRstStream(ctx, STREAM_ID, PROTOCOL_ERROR.code(), promise); } @Test @@ -502,14 +502,14 @@ public void serverShouldNeverSend431IfHeadersAlreadySent() throws Exception { when(connection.isServer()).thenReturn(true); when(stream.isHeadersSent()).thenReturn(true); when(remote.lastStreamCreated()).thenReturn(STREAM_ID); - when(frameWriter.writeRstStream(eq(ctx), eq(STREAM_ID), + when(encoder.writeRstStream(eq(ctx), eq(STREAM_ID), eq(PROTOCOL_ERROR.code()), eq(promise))).thenReturn(future); handler.exceptionCaught(ctx, e); verify(encoder, never()).writeHeaders(eq(ctx), eq(STREAM_ID), any(Http2Headers.class), eq(padding), eq(true), eq(promise)); - verify(frameWriter).writeRstStream(ctx, STREAM_ID, PROTOCOL_ERROR.code(), promise); + verify(encoder).writeRstStream(ctx, STREAM_ID, PROTOCOL_ERROR.code(), promise); } @Test @@ -526,15 +526,15 @@ public void serverShouldCreateStreamIfNeededBeforeSending431() throws Exception when(connection.isServer()).thenReturn(true); when(stream.isHeadersSent()).thenReturn(false); when(remote.lastStreamCreated()).thenReturn(STREAM_ID); - when(frameWriter.writeRstStream(eq(ctx), eq(STREAM_ID), + when(encoder.writeRstStream(eq(ctx), eq(STREAM_ID), eq(PROTOCOL_ERROR.code()), eq(promise))).thenReturn(future); handler.exceptionCaught(ctx, e); verify(remote).createStream(STREAM_ID, true); verify(encoder).writeHeaders(eq(ctx), eq(STREAM_ID), any(Http2Headers.class), eq(padding), eq(true), eq(promise)); - verify(frameWriter).writeRstStream(ctx, STREAM_ID, PROTOCOL_ERROR.code(), promise); + verify(encoder).writeRstStream(ctx, STREAM_ID, PROTOCOL_ERROR.code(), promise); } @Test
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-prj3-ccx8-p6x4ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-55163ghsaADVISORY
- www.openwall.com/lists/oss-security/2025/08/16/1ghsaWEB
- github.com/grpc/grpc-java/commit/6462ef9a11980e168c21d90bbc7245c728fd1a7aghsaWEB
- github.com/netty/netty/commit/be53dc3c9acd9af2e20d0c3c07cd77115a594cf1ghsaWEB
- github.com/netty/netty/security/advisories/GHSA-prj3-ccx8-p6x4ghsax_refsource_CONFIRMWEB
- www.kb.cert.org/vuls/id/767506ghsaWEB
News mentions
0No linked articles in our index yet.