CVE-2023-44487: Uncontrolled Resource Consumption (CWE-400)
value: The HTTP/2 protocol allows a denial of service (server resource consumption) because request cancellation can reset many streams quickly, as exploited in the wild in August through October 2023.
CWE description: The product does not properly control the allocation and maintenance of a limited resource, thereby enabling an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources.
CVSS v3 base score: 7.5
Published: 2023-10-10T14:15Z
Repository: https://github.com/netty/netty
Language: Java
Files changed: codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2ConnectionHandlerBuilder.java, codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodecBuilder.java, codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MaxRstFrameDecoder.java, codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MaxRstFrameListener.java, codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexCodecBuilder.java, codec-http2/src/test/java/io/netty/handler/codec/http2/AbstractDecoratingHttp2ConnectionDecoderTest.java, codec-http2/src/test/java/io/netty/handler/codec/http2/Http2EmptyDataFrameConnectionDecoderTest.java, codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MaxRstFrameConnectionDecoderTest.java, codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MaxRstFrameListenerTest.java
Commit date: 2023-10-10 05:47:24 -0700
Commit message: Merge pull request from GHSA-xpw8-rcwv-8f8p

Motivation:

It's possible for a remote peer to overload a remote system by issue a huge amount of RST frames. While this is completely valid in terms of the RFC we need to limit the amount to protect against DDOS attacks.

Modifications:

Add protection against RST floods which is enabled by default.

Result:

Protect against DDOS caused by RST floods (CVE-2023-44487)
Diff: diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2ConnectionHandlerBuilder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2ConnectionHandlerBuilder.java
index 5d42042dbf2..b7c9b9a5e4e 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2ConnectionHandlerBuilder.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2ConnectionHandlerBuilder.java
@@ -107,10 +107,12 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne
     private Http2PromisedRequestVerifier promisedRequestVerifier = ALWAYS_VERIFY;
     private boolean autoAckSettingsFrame = true;
     private boolean autoAckPingFrame = true;
     private int maxQueuedControlFrames = Http2CodecUtil.DEFAULT_MAX_QUEUED_CONTROL_FRAMES;
     private int maxConsecutiveEmptyFrames = 2;
+    private int maxRstFramesPerWindow = 200;
+    private int secondsPerWindow = 30;
 
     /**
      * Sets the {@link Http2Settings} to use for the initial connection settings exchange.
      */
     protected Http2Settings initialSettings() {
@@ -408,22 +410,22 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne
         return promisedRequestVerifier;
     }
 
     /**
      * Returns the maximum number of consecutive empty DATA frames (without end_of_stream flag) that are allowed before
-     * the connection is closed. This allows to protected against the remote peer flooding us with such frames and
+     * the connection is closed. This allows to protect against the remote peer flooding us with such frames and
      * so use up a lot of CPU. There is no valid use-case for empty DATA frames without end_of_stream flag.
      *
      * {@code 0} means no protection is in place.
      */
     protected int decoderEnforceMaxConsecutiveEmptyDataFrames() {
         return maxConsecutiveEmptyFrames;
     }
 
     /**
      * Sets the maximum number of consecutive empty DATA frames (without end_of_stream flag) that are allowed before
-     * the connection is closed. This allows to protected against the remote peer flooding us with such frames and
+     * the connection is closed. This allows to protect against the remote peer flooding us with such frames and
      * so use up a lot of CPU. There is no valid use-case for empty DATA frames without end_of_stream flag.
      *
      * {@code 0} means no protection should be applied.
      */
     protected B decoderEnforceMaxConsecutiveEmptyDataFrames(int maxConsecutiveEmptyFrames) {
@@ -431,10 +433,25 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne
         this.maxConsecutiveEmptyFrames = checkPositiveOrZero(
                 maxConsecutiveEmptyFrames, "maxConsecutiveEmptyFrames");
         return self();
     }
 
+    /**
+     * Sets the maximum number RST frames that are allowed per window before
+     * the connection is closed. This allows to protect against the remote peer flooding us with such frames and
+     * so use up a lot of CPU.
+     *
+     * {@code 0} for any of the parameters means no protection should be applied.
+     */
+    protected B decoderEnforceMaxRstFramesPerWindow(int maxRstFramesPerWindow, int secondsPerWindow) {
+        enforceNonCodecConstraints("decoderEnforceMaxRstFramesPerWindow");
+        this.maxRstFramesPerWindow = checkPositiveOrZero(
+                maxRstFramesPerWindow, "maxRstFramesPerWindow");
+        this.secondsPerWindow = checkPositiveOrZero(secondsPerWindow, "secondsPerWindow");
+        return self();
+    }
+
     /**
      * Determine if settings frame should automatically be acknowledged and applied.
      * @return this.
      */
     protected B autoAckSettingsFrame(boolean autoAckSettings) {
@@ -573,10 +590,13 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne
     private T buildFromCodec(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder) {
         int maxConsecutiveEmptyDataFrames = decoderEnforceMaxConsecutiveEmptyDataFrames();
         if (maxConsecutiveEmptyDataFrames > 0) {
             decoder = new Http2EmptyDataFrameConnectionDecoder(decoder, maxConsecutiveEmptyDataFrames);
         }
+        if (maxRstFramesPerWindow > 0 && secondsPerWindow > 0) {
+            decoder = new Http2MaxRstFrameDecoder(decoder, maxRstFramesPerWindow, secondsPerWindow);
+        }
         final T handler;
         try {
             // Call the abstract build method
             handler = build(decoder, encoder, initialSettings);
         } catch (Throwable t) {
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodecBuilder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodecBuilder.java
index 3348ee60281..398aac44f17 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodecBuilder.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodecBuilder.java
@@ -192,10 +192,16 @@ public class Http2FrameCodecBuilder extends
     @Override
     public Http2FrameCodecBuilder decoderEnforceMaxConsecutiveEmptyDataFrames(int maxConsecutiveEmptyFrames) {
         return super.decoderEnforceMaxConsecutiveEmptyDataFrames(maxConsecutiveEmptyFrames);
     }
 
+    @Override
+    public Http2FrameCodecBuilder decoderEnforceMaxRstFramesPerWindow(
+            int maxConsecutiveEmptyFrames, int secondsPerWindow) {
+        return super.decoderEnforceMaxRstFramesPerWindow(maxConsecutiveEmptyFrames, secondsPerWindow);
+    }
+
     /**
      * Build a {@link Http2FrameCodec} object.
      */
     @Override
     public Http2FrameCodec build() {
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MaxRstFrameDecoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MaxRstFrameDecoder.java
new file mode 100644
index 00000000000..6ac6660280e
--- /dev/null
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MaxRstFrameDecoder.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2023 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.http2;
+
+import static io.netty.util.internal.ObjectUtil.checkPositive;
+
+
+/**
+ * Enforce a limit on the maximum number of RST frames that are allowed per a window
+ * before the connection will be closed with a GO_AWAY frame.
+ */
+final class Http2MaxRstFrameDecoder extends DecoratingHttp2ConnectionDecoder {
+    private final int maxRstFramesPerWindow;
+    private final int secondsPerWindow;
+
+    Http2MaxRstFrameDecoder(Http2ConnectionDecoder delegate, int maxRstFramesPerWindow, int secondsPerWindow) {
+        super(delegate);
+        this.maxRstFramesPerWindow = checkPositive(maxRstFramesPerWindow, "maxRstFramesPerWindow");
+        this.secondsPerWindow = checkPositive(secondsPerWindow, "secondsPerWindow");
+    }
+
+    @Override
+    public void frameListener(Http2FrameListener listener) {
+        if (listener != null) {
+            super.frameListener(new Http2MaxRstFrameListener(listener, maxRstFramesPerWindow, secondsPerWindow));
+        } else {
+            super.frameListener(null);
+        }
+    }
+
+    @Override
+    public Http2FrameListener frameListener() {
+        Http2FrameListener frameListener = frameListener0();
+        // Unwrap the original Http2FrameListener as we add this decoder under the hood.
+        if (frameListener instanceof Http2MaxRstFrameListener) {
+            return ((Http2MaxRstFrameListener) frameListener).listener;
+        }
+        return frameListener;
+    }
+
+    // Package-private for testing
+    Http2FrameListener frameListener0() {
+        return super.frameListener();
+    }
+}
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MaxRstFrameListener.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MaxRstFrameListener.java
new file mode 100644
index 00000000000..4603686183a
--- /dev/null
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MaxRstFrameListener.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2023 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.http2;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.util.internal.logging.InternalLogger;
+import io.netty.util.internal.logging.InternalLoggerFactory;
+
+import java.util.concurrent.TimeUnit;
+
+
+final class Http2MaxRstFrameListener extends Http2FrameListenerDecorator {
+    private static final InternalLogger logger = InternalLoggerFactory.getInstance(Http2MaxRstFrameListener.class);
+
+    private final long nanosPerWindow;
+    private final int maxRstFramesPerWindow;
+    private long lastRstFrameNano = System.nanoTime();
+    private int receivedRstInWindow;
+
+    Http2MaxRstFrameListener(Http2FrameListener listener, int maxRstFramesPerWindow, int secondsPerWindow) {
+        super(listener);
+        this.maxRstFramesPerWindow = maxRstFramesPerWindow;
+        this.nanosPerWindow = TimeUnit.SECONDS.toNanos(secondsPerWindow);
+    }
+
+    @Override
+    public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) throws Http2Exception {
+        long currentNano = System.nanoTime();
+        if (currentNano - lastRstFrameNano >= nanosPerWindow) {
+            lastRstFrameNano = currentNano;
+            receivedRstInWindow = 1;
+        } else {
+            receivedRstInWindow++;
+            if (receivedRstInWindow > maxRstFramesPerWindow) {
+                Http2Exception exception = Http2Exception.connectionError(Http2Error.ENHANCE_YOUR_CALM,
+                        "Maximum number of RST frames reached");
+                logger.debug("{} Maximum number {} of RST frames reached within {} seconds, " +
+                                "closing connection with {} error", ctx.channel(), maxRstFramesPerWindow,
+                        TimeUnit.NANOSECONDS.toSeconds(nanosPerWindow), exception.error(), exception);
+                throw exception;
+            }
+        }
+        super.onRstStreamRead(ctx, streamId, errorCode);
+    }
+}
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexCodecBuilder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexCodecBuilder.java
index 02515af175d..6948e96c4c2 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexCodecBuilder.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexCodecBuilder.java
@@ -209,10 +209,16 @@ public class Http2MultiplexCodecBuilder
     @Override
     public Http2MultiplexCodecBuilder decoderEnforceMaxConsecutiveEmptyDataFrames(int maxConsecutiveEmptyFrames) {
         return super.decoderEnforceMaxConsecutiveEmptyDataFrames(maxConsecutiveEmptyFrames);
     }
 
+    @Override
+    public Http2MultiplexCodecBuilder decoderEnforceMaxRstFramesPerWindow(
+            int maxConsecutiveEmptyFrames, int secondsPerWindow) {
+        return super.decoderEnforceMaxRstFramesPerWindow(maxConsecutiveEmptyFrames, secondsPerWindow);
+    }
+
     @Override
     public Http2MultiplexCodec build() {
         Http2FrameWriter frameWriter = this.frameWriter;
         if (frameWriter != null) {
             // This is to support our tests and will never be executed by the user as frameWriter(...)
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/AbstractDecoratingHttp2ConnectionDecoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/AbstractDecoratingHttp2ConnectionDecoderTest.java
new file mode 100644
index 00000000000..cd5e43b1305
--- /dev/null
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/AbstractDecoratingHttp2ConnectionDecoderTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2023 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.http2;
+
+import org.hamcrest.CoreMatchers;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public abstract class AbstractDecoratingHttp2ConnectionDecoderTest {
+
+    protected abstract DecoratingHttp2ConnectionDecoder newDecoder(Http2ConnectionDecoder decoder);
+
+    protected abstract Class<? extends Http2FrameListener> delegatingFrameListenerType();
+
+    @Test
+    public void testDecoration() {
+        Http2ConnectionDecoder delegate = mock(Http2ConnectionDecoder.class);
+        final ArgumentCaptor<Http2FrameListener> listenerArgumentCaptor =
+                ArgumentCaptor.forClass(Http2FrameListener.class);
+        when(delegate.frameListener()).then(new Answer<Http2FrameListener>() {
+            @Override
+            public Http2FrameListener answer(InvocationOnMock invocationOnMock) {
+                return listenerArgumentCaptor.getValue();
+            }
+        });
+        Http2FrameListener listener = mock(Http2FrameListener.class);
+        DecoratingHttp2ConnectionDecoder decoder = newDecoder(delegate);
+        decoder.frameListener(listener);
+        verify(delegate).frameListener(listenerArgumentCaptor.capture());
+
+        assertThat(decoder.frameListener(),
+                CoreMatchers.not(CoreMatchers.instanceOf(delegatingFrameListenerType())));
+    }
+
+    @Test
+    public void testDecorationWithNull() {
+        Http2ConnectionDecoder delegate = mock(Http2ConnectionDecoder.class);
+
+        DecoratingHttp2ConnectionDecoder decoder = newDecoder(delegate);
+        decoder.frameListener(null);
+        assertNull(decoder.frameListener());
+    }
+}
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2EmptyDataFrameConnectionDecoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2EmptyDataFrameConnectionDecoderTest.java
index 231aa7cd44a..901db88f33b 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2EmptyDataFrameConnectionDecoderTest.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2EmptyDataFrameConnectionDecoderTest.java
@@ -12,49 +12,17 @@
  * or implied. See the License for the specific language governing permissions and limitations under
  * the License.
  */
 package io.netty.handler.codec.http2;
 
-import org.hamcrest.CoreMatchers;
-import org.junit.jupiter.api.Test;
-import org.mockito.ArgumentCaptor;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
+public class Http2EmptyDataFrameConnectionDecoderTest extends AbstractDecoratingHttp2ConnectionDecoderTest {
 
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-public class Http2EmptyDataFrameConnectionDecoderTest {
-
-    @Test
-    public void testDecoration() {
-        Http2ConnectionDecoder delegate = mock(Http2ConnectionDecoder.class);
-        final ArgumentCaptor<Http2FrameListener> listenerArgumentCaptor =
-                ArgumentCaptor.forClass(Http2FrameListener.class);
-        when(delegate.frameListener()).then(new Answer<Http2FrameListener>() {
-            @Override
-            public Http2FrameListener answer(InvocationOnMock invocationOnMock) {
-                return listenerArgumentCaptor.getValue();
-            }
-        });
-        Http2FrameListener listener = mock(Http2FrameListener.class);
-        Http2EmptyDataFrameConnectionDecoder decoder = new Http2EmptyDataFrameConnectionDecoder(delegate, 2);
-        decoder.frameListener(listener);
-        verify(delegate).frameListener(listenerArgumentCaptor.capture());
-
-        assertThat(decoder.frameListener(),
-                CoreMatchers.not(CoreMatchers.instanceOf(Http2EmptyDataFrameListener.class)));
-        assertThat(decoder.frameListener0(), CoreMatchers.instanceOf(Http2EmptyDataFrameListener.class));
+    @Override
+    protected DecoratingHttp2ConnectionDecoder newDecoder(Http2ConnectionDecoder decoder) {
+        return new Http2EmptyDataFrameConnectionDecoder(decoder, 2);
     }
 
-    @Test
-    public void testDecorationWithNull() {
-        Http2ConnectionDecoder delegate = mock(Http2ConnectionDecoder.class);
-
-        Http2EmptyDataFrameConnectionDecoder decoder = new Http2EmptyDataFrameConnectionDecoder(delegate, 2);
-        decoder.frameListener(null);
-        assertNull(decoder.frameListener());
+    @Override
+    protected Class<? extends Http2FrameListener> delegatingFrameListenerType() {
+        return Http2EmptyDataFrameListener.class;
     }
 }
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MaxRstFrameConnectionDecoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MaxRstFrameConnectionDecoderTest.java
new file mode 100644
index 00000000000..5ec35386d58
--- /dev/null
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MaxRstFrameConnectionDecoderTest.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2023 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.http2;
+
+public class Http2MaxRstFrameConnectionDecoderTest extends AbstractDecoratingHttp2ConnectionDecoderTest {
+
+    @Override
+    protected DecoratingHttp2ConnectionDecoder newDecoder(Http2ConnectionDecoder decoder) {
+        return new Http2MaxRstFrameDecoder(decoder, 200, 30);
+    }
+
+    @Override
+    protected Class<? extends Http2FrameListener> delegatingFrameListenerType() {
+        return Http2MaxRstFrameListener.class;
+    }
+}
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MaxRstFrameListenerTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MaxRstFrameListenerTest.java
new file mode 100644
index 00000000000..381834bc348
--- /dev/null
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MaxRstFrameListenerTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2023 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.http2;
+
+import io.netty.channel.ChannelHandlerContext;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
+import org.mockito.Mock;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+public class Http2MaxRstFrameListenerTest {
+
+    @Mock
+    private Http2FrameListener frameListener;
+    @Mock
+    private ChannelHandlerContext ctx;
+
+    private Http2MaxRstFrameListener listener;
+
+    @BeforeEach
+    public void setUp() {
+        initMocks(this);
+    }
+
+    @Test
+    public void testMaxRstFramesReached() throws Http2Exception {
+        listener = new Http2MaxRstFrameListener(frameListener, 1, 10);
+        listener.onRstStreamRead(ctx, 1, Http2Error.STREAM_CLOSED.code());
+
+        Http2Exception ex = assertThrows(Http2Exception.class, new Executable() {
+            @Override
+            public void execute() throws Throwable {
+                listener.onRstStreamRead(ctx, 2, Http2Error.STREAM_CLOSED.code());
+            }
+        });
+        assertEquals(Http2Error.ENHANCE_YOUR_CALM, ex.error());
+        verify(frameListener, times(1)).onRstStreamRead(eq(ctx), anyInt(), eq(Http2Error.STREAM_CLOSED.code()));
+    }
+
+    @Test
+    public void testRstFrames() throws Exception {
+        listener = new Http2MaxRstFrameListener(frameListener, 1, 1);
+        listener.onRstStreamRead(ctx, 1, Http2Error.STREAM_CLOSED.code());
+        Thread.sleep(1100);
+        listener.onRstStreamRead(ctx, 1, Http2Error.STREAM_CLOSED.code());
+        verify(frameListener, times(2)).onRstStreamRead(eq(ctx), anyInt(), eq(Http2Error.STREAM_CLOSED.code()));
+    }
+}

Vulnerable code:      * the connection is closed. This allows to protected against the remote peer flooding us with such frames and
     * the connection is closed. This allows to protected against the remote peer flooding us with such frames and
import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class Http2EmptyDataFrameConnectionDecoderTest {

    @Test
    public void testDecoration() {
        Http2ConnectionDecoder delegate = mock(Http2ConnectionDecoder.class);
        final ArgumentCaptor<Http2FrameListener> listenerArgumentCaptor =
                ArgumentCaptor.forClass(Http2FrameListener.class);
        when(delegate.frameListener()).then(new Answer<Http2FrameListener>() {
            @Override
            public Http2FrameListener answer(InvocationOnMock invocationOnMock) {
                return listenerArgumentCaptor.getValue();
            }
        });
        Http2FrameListener listener = mock(Http2FrameListener.class);
        Http2EmptyDataFrameConnectionDecoder decoder = new Http2EmptyDataFrameConnectionDecoder(delegate, 2);
        decoder.frameListener(listener);
        verify(delegate).frameListener(listenerArgumentCaptor.capture());

        assertThat(decoder.frameListener(),
                CoreMatchers.not(CoreMatchers.instanceOf(Http2EmptyDataFrameListener.class)));
        assertThat(decoder.frameListener0(), CoreMatchers.instanceOf(Http2EmptyDataFrameListener.class));
    @Test
    public void testDecorationWithNull() {
        Http2ConnectionDecoder delegate = mock(Http2ConnectionDecoder.class);

        Http2EmptyDataFrameConnectionDecoder decoder = new Http2EmptyDataFrameConnectionDecoder(delegate, 2);
        decoder.frameListener(null);
        assertNull(decoder.frameListener());
Fixed code:     private int maxRstFramesPerWindow = 200;
    private int secondsPerWindow = 30;
     * the connection is closed. This allows to protect against the remote peer flooding us with such frames and
     * the connection is closed. This allows to protect against the remote peer flooding us with such frames and
    /**
     * Sets the maximum number RST frames that are allowed per window before
     * the connection is closed. This allows to protect against the remote peer flooding us with such frames and
     * so use up a lot of CPU.
     *
     * {@code 0} for any of the parameters means no protection should be applied.
     */
    protected B decoderEnforceMaxRstFramesPerWindow(int maxRstFramesPerWindow, int secondsPerWindow) {
        enforceNonCodecConstraints("decoderEnforceMaxRstFramesPerWindow");
        this.maxRstFramesPerWindow = checkPositiveOrZero(
                maxRstFramesPerWindow, "maxRstFramesPerWindow");
        this.secondsPerWindow = checkPositiveOrZero(secondsPerWindow, "secondsPerWindow");
        return self();
    }

        if (maxRstFramesPerWindow > 0 && secondsPerWindow > 0) {
            decoder = new Http2MaxRstFrameDecoder(decoder, maxRstFramesPerWindow, secondsPerWindow);
        }
    @Override
    public Http2FrameCodecBuilder decoderEnforceMaxRstFramesPerWindow(
            int maxConsecutiveEmptyFrames, int secondsPerWindow) {
        return super.decoderEnforceMaxRstFramesPerWindow(maxConsecutiveEmptyFrames, secondsPerWindow);
    }

/*
 * Copyright 2023 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.http2;

import static io.netty.util.internal.ObjectUtil.checkPositive;


/**
 * Enforce a limit on the maximum number of RST frames that are allowed per a window
 * before the connection will be closed with a GO_AWAY frame.
 */
final class Http2MaxRstFrameDecoder extends DecoratingHttp2ConnectionDecoder {
    private final int maxRstFramesPerWindow;
    private final int secondsPerWindow;

    Http2MaxRstFrameDecoder(Http2ConnectionDecoder delegate, int maxRstFramesPerWindow, int secondsPerWindow) {
        super(delegate);
        this.maxRstFramesPerWindow = checkPositive(maxRstFramesPerWindow, "maxRstFramesPerWindow");
        this.secondsPerWindow = checkPositive(secondsPerWindow, "secondsPerWindow");
    }

    @Override
    public void frameListener(Http2FrameListener listener) {
        if (listener != null) {
            super.frameListener(new Http2MaxRstFrameListener(listener, maxRstFramesPerWindow, secondsPerWindow));
        } else {
            super.frameListener(null);
        }
    }

    @Override
    public Http2FrameListener frameListener() {
        Http2FrameListener frameListener = frameListener0();
        // Unwrap the original Http2FrameListener as we add this decoder under the hood.
        if (frameListener instanceof Http2MaxRstFrameListener) {
            return ((Http2MaxRstFrameListener) frameListener).listener;
        }
        return frameListener;
    }

    // Package-private for testing
    Http2FrameListener frameListener0() {
        return super.frameListener();
    }
}
/*
 * Copyright 2023 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.http2;

import io.netty.channel.ChannelHandlerContext;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;

import java.util.concurrent.TimeUnit;


final class Http2MaxRstFrameListener extends Http2FrameListenerDecorator {
    private static final InternalLogger logger = InternalLoggerFactory.getInstance(Http2MaxRstFrameListener.class);

    private final long nanosPerWindow;
    private final int maxRstFramesPerWindow;
    private long lastRstFrameNano = System.nanoTime();
    private int receivedRstInWindow;

    Http2MaxRstFrameListener(Http2FrameListener listener, int maxRstFramesPerWindow, int secondsPerWindow) {
        super(listener);
        this.maxRstFramesPerWindow = maxRstFramesPerWindow;
        this.nanosPerWindow = TimeUnit.SECONDS.toNanos(secondsPerWindow);
    }

    @Override
    public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) throws Http2Exception {
        long currentNano = System.nanoTime();
        if (currentNano - lastRstFrameNano >= nanosPerWindow) {
            lastRstFrameNano = currentNano;
            receivedRstInWindow = 1;
        } else {
            receivedRstInWindow++;
            if (receivedRstInWindow > maxRstFramesPerWindow) {
                Http2Exception exception = Http2Exception.connectionError(Http2Error.ENHANCE_YOUR_CALM,
                        "Maximum number of RST frames reached");
                logger.debug("{} Maximum number {} of RST frames reached within {} seconds, " +
                                "closing connection with {} error", ctx.channel(), maxRstFramesPerWindow,
                        TimeUnit.NANOSECONDS.toSeconds(nanosPerWindow), exception.error(), exception);
                throw exception;
            }
        }
        super.onRstStreamRead(ctx, streamId, errorCode);
    }
}
    @Override
    public Http2MultiplexCodecBuilder decoderEnforceMaxRstFramesPerWindow(
            int maxConsecutiveEmptyFrames, int secondsPerWindow) {
        return super.decoderEnforceMaxRstFramesPerWindow(maxConsecutiveEmptyFrames, secondsPerWindow);
    }

/*
 * Copyright 2023 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.http2;

import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import static org.junit.jupiter.api.Assertions.assertNull;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public abstract class AbstractDecoratingHttp2ConnectionDecoderTest {

    protected abstract DecoratingHttp2ConnectionDecoder newDecoder(Http2ConnectionDecoder decoder);

    protected abstract Class<? extends Http2FrameListener> delegatingFrameListenerType();

    @Test
    public void testDecoration() {
        Http2ConnectionDecoder delegate = mock(Http2ConnectionDecoder.class);
        final ArgumentCaptor<Http2FrameListener> listenerArgumentCaptor =
                ArgumentCaptor.forClass(Http2FrameListener.class);
        when(delegate.frameListener()).then(new Answer<Http2FrameListener>() {
            @Override
            public Http2FrameListener answer(InvocationOnMock invocationOnMock) {
                return listenerArgumentCaptor.getValue();
            }
        });
        Http2FrameListener listener = mock(Http2FrameListener.class);
        DecoratingHttp2ConnectionDecoder decoder = newDecoder(delegate);
        decoder.frameListener(listener);
        verify(delegate).frameListener(listenerArgumentCaptor.capture());

        assertThat(decoder.frameListener(),
                CoreMatchers.not(CoreMatchers.instanceOf(delegatingFrameListenerType())));
    }

    @Test
    public void testDecorationWithNull() {
        Http2ConnectionDecoder delegate = mock(Http2ConnectionDecoder.class);

        DecoratingHttp2ConnectionDecoder decoder = newDecoder(delegate);
        decoder.frameListener(null);
        assertNull(decoder.frameListener());
    }
}
public class Http2EmptyDataFrameConnectionDecoderTest extends AbstractDecoratingHttp2ConnectionDecoderTest {
    @Override
    protected DecoratingHttp2ConnectionDecoder newDecoder(Http2ConnectionDecoder decoder) {
        return new Http2EmptyDataFrameConnectionDecoder(decoder, 2);
    @Override
    protected Class<? extends Http2FrameListener> delegatingFrameListenerType() {
        return Http2EmptyDataFrameListener.class;
/*
 * Copyright 2023 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.http2;

public class Http2MaxRstFrameConnectionDecoderTest extends AbstractDecoratingHttp2ConnectionDecoderTest {

    @Override
    protected DecoratingHttp2ConnectionDecoder newDecoder(Http2ConnectionDecoder decoder) {
        return new Http2MaxRstFrameDecoder(decoder, 200, 30);
    }

    @Override
    protected Class<? extends Http2FrameListener> delegatingFrameListenerType() {
        return Http2MaxRstFrameListener.class;
    }
}
/*
 * Copyright 2023 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.http2;

import io.netty.channel.ChannelHandlerContext;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
import org.mockito.Mock;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.MockitoAnnotations.initMocks;

public class Http2MaxRstFrameListenerTest {

    @Mock
    private Http2FrameListener frameListener;
    @Mock
    private ChannelHandlerContext ctx;

    private Http2MaxRstFrameListener listener;

    @BeforeEach
    public void setUp() {
        initMocks(this);
    }

    @Test
    public void testMaxRstFramesReached() throws Http2Exception {
        listener = new Http2MaxRstFrameListener(frameListener, 1, 10);
        listener.onRstStreamRead(ctx, 1, Http2Error.STREAM_CLOSED.code());

        Http2Exception ex = assertThrows(Http2Exception.class, new Executable() {
            @Override
            public void execute() throws Throwable {
                listener.onRstStreamRead(ctx, 2, Http2Error.STREAM_CLOSED.code());
            }
        });
        assertEquals(Http2Error.ENHANCE_YOUR_CALM, ex.error());
        verify(frameListener, times(1)).onRstStreamRead(eq(ctx), anyInt(), eq(Http2Error.STREAM_CLOSED.code()));
    }

    @Test
    public void testRstFrames() throws Exception {
        listener = new Http2MaxRstFrameListener(frameListener, 1, 1);
        listener.onRstStreamRead(ctx, 1, Http2Error.STREAM_CLOSED.code());
        Thread.sleep(1100);
        listener.onRstStreamRead(ctx, 1, Http2Error.STREAM_CLOSED.code());
        verify(frameListener, times(2)).onRstStreamRead(eq(ctx), anyInt(), eq(Http2Error.STREAM_CLOSED.code()));
    }
}