CVE-2023-44487 (published 2023-10-10T14:15Z)

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.

CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)

Weakness information:
cwe_id: CWE-400
cwe_name: Uncontrolled Resource Consumption
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.
extended_description: 
            Limited resources include memory, file system storage, database connection pool entries, and CPU. If an attacker can trigger the allocation of these limited resources, but the number or size of the resources is not controlled, then the attacker could cause a denial of service that consumes all available resources. This would prevent valid users from accessing the product, and it could potentially have an impact on the surrounding environment. For example, a memory exhaustion attack against an application could slow down the application as well as its host operating system.
            There are at least three distinct scenarios which can commonly lead to resource exhaustion:
               
                  Lack of throttling for the number of allocated resources
                  Losing all references to a resource before reaching the shutdown stage
                  Not closing/returning a resource after processing
               
            Resource exhaustion problems are often result due to an incorrect implementation of the following situations:
               
                  Error conditions and other exceptional circumstances.
                  Confusion over which part of the program is responsible for releasing the resource.
               
         
         

Fix information:
repo_url: https://github.com/netty/netty
commit_details: repo_url: https://github.com/netty/netty
author: Norman Maurer
author_date: 2023-10-10 05:47:24-07:00
msg: 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)
repository: repo_url: https://github.com/netty/netty
repo_name: netty/netty
description: Netty project - an event-driven asynchronous network application framework
repo_language: Java
owner: netty
file_changes: filename: AbstractHttp2ConnectionHandlerBuilder.java
change_type: ModificationType.MODIFY
diff: @@ -109,6 +109,8 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne
     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.
@@ -410,7 +412,7 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne
 
     /**
      * 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.
@@ -421,7 +423,7 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne
 
     /**
      * 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.
@@ -433,6 +435,21 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne
         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.
@@ -575,6 +592,9 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne
         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

programming_language: Java
filename: Http2FrameCodecBuilder.java
change_type: ModificationType.MODIFY
diff: @@ -194,6 +194,12 @@ public class Http2FrameCodecBuilder extends
         return super.decoderEnforceMaxConsecutiveEmptyDataFrames(maxConsecutiveEmptyFrames);
     }
 
+    @Override
+    public Http2FrameCodecBuilder decoderEnforceMaxRstFramesPerWindow(
+            int maxConsecutiveEmptyFrames, int secondsPerWindow) {
+        return super.decoderEnforceMaxRstFramesPerWindow(maxConsecutiveEmptyFrames, secondsPerWindow);
+    }
+
     /**
      * Build a {@link Http2FrameCodec} object.
      */

programming_language: Java
filename: Http2MaxRstFrameDecoder.java
change_type: ModificationType.ADD
diff: @@ -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();
+    }
+}

programming_language: Java
filename: Http2MaxRstFrameListener.java
change_type: ModificationType.ADD
diff: @@ -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);
+    }
+}

programming_language: Java
filename: Http2MultiplexCodecBuilder.java
change_type: ModificationType.MODIFY
diff: @@ -211,6 +211,12 @@ public class Http2MultiplexCodecBuilder
         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;

programming_language: Java
filename: AbstractDecoratingHttp2ConnectionDecoderTest.java
change_type: ModificationType.ADD
diff: @@ -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());
+    }
+}

programming_language: Java
filename: Http2EmptyDataFrameConnectionDecoderTest.java
change_type: ModificationType.MODIFY
diff: @@ -14,47 +14,15 @@
  */
 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;
     }
 }

programming_language: Java
filename: Http2MaxRstFrameConnectionDecoderTest.java
change_type: ModificationType.ADD
diff: @@ -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;
+    }
+}

programming_language: Java
filename: Http2MaxRstFrameListenerTest.java
change_type: ModificationType.ADD
diff: @@ -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()));
+    }
+}

programming_language: Java