From 58f75f665aa81a8cbcf6ffa74820042a285c5e61 Mon Sep 17 00:00:00 2001 From: Norman Maurer Date: Tue, 10 Oct 2023 05:47:24 -0700 Subject: [PATCH] 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) --- ...AbstractHttp2ConnectionHandlerBuilder.java | 24 ++++++- .../codec/http2/Http2FrameCodecBuilder.java | 6 ++ .../codec/http2/Http2MaxRstFrameDecoder.java | 58 ++++++++++++++++ .../codec/http2/Http2MaxRstFrameListener.java | 58 ++++++++++++++++ .../http2/Http2MultiplexCodecBuilder.java | 6 ++ ...tDecoratingHttp2ConnectionDecoderTest.java | 63 +++++++++++++++++ ...p2EmptyDataFrameConnectionDecoderTest.java | 46 ++----------- ...Http2MaxRstFrameConnectionDecoderTest.java | 28 ++++++++ .../http2/Http2MaxRstFrameListenerTest.java | 68 +++++++++++++++++++ 9 files changed, 316 insertions(+), 41 deletions(-) create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MaxRstFrameDecoder.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MaxRstFrameListener.java create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/AbstractDecoratingHttp2ConnectionDecoderTest.java create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MaxRstFrameConnectionDecoderTest.java create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/Http2MaxRstFrameListenerTest.java 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 @@ -109,6 +109,8 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder 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 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 @@ -194,6 +194,12 @@ public Http2FrameCodecBuilder decoderEnforceMaxConsecutiveEmptyDataFrames(int ma return super.decoderEnforceMaxConsecutiveEmptyDataFrames(maxConsecutiveEmptyFrames); } + @Override + public Http2FrameCodecBuilder decoderEnforceMaxRstFramesPerWindow( + int maxConsecutiveEmptyFrames, int secondsPerWindow) { + return super.decoderEnforceMaxRstFramesPerWindow(maxConsecutiveEmptyFrames, secondsPerWindow); + } + /** * Build a {@link Http2FrameCodec} object. */ 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 @@ -211,6 +211,12 @@ public Http2MultiplexCodecBuilder decoderEnforceMaxConsecutiveEmptyDataFrames(in 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; 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 delegatingFrameListenerType(); + + @Test + public void testDecoration() { + Http2ConnectionDecoder delegate = mock(Http2ConnectionDecoder.class); + final ArgumentCaptor listenerArgumentCaptor = + ArgumentCaptor.forClass(Http2FrameListener.class); + when(delegate.frameListener()).then(new Answer() { + @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 @@ -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 listenerArgumentCaptor = - ArgumentCaptor.forClass(Http2FrameListener.class); - when(delegate.frameListener()).then(new Answer() { - @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 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 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())); + } +}