Skip to content

Commit 58f75f6

Browse files
authored
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)
1 parent 4911448 commit 58f75f6

9 files changed

+316
-41
lines changed

Diff for: codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2ConnectionHandlerBuilder.java

+22-2
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne
109109
private boolean autoAckPingFrame = true;
110110
private int maxQueuedControlFrames = Http2CodecUtil.DEFAULT_MAX_QUEUED_CONTROL_FRAMES;
111111
private int maxConsecutiveEmptyFrames = 2;
112+
private int maxRstFramesPerWindow = 200;
113+
private int secondsPerWindow = 30;
112114

113115
/**
114116
* Sets the {@link Http2Settings} to use for the initial connection settings exchange.
@@ -410,7 +412,7 @@ protected Http2PromisedRequestVerifier promisedRequestVerifier() {
410412

411413
/**
412414
* Returns the maximum number of consecutive empty DATA frames (without end_of_stream flag) that are allowed before
413-
* the connection is closed. This allows to protected against the remote peer flooding us with such frames and
415+
* the connection is closed. This allows to protect against the remote peer flooding us with such frames and
414416
* so use up a lot of CPU. There is no valid use-case for empty DATA frames without end_of_stream flag.
415417
*
416418
* {@code 0} means no protection is in place.
@@ -421,7 +423,7 @@ protected int decoderEnforceMaxConsecutiveEmptyDataFrames() {
421423

422424
/**
423425
* Sets the maximum number of consecutive empty DATA frames (without end_of_stream flag) that are allowed before
424-
* the connection is closed. This allows to protected against the remote peer flooding us with such frames and
426+
* the connection is closed. This allows to protect against the remote peer flooding us with such frames and
425427
* so use up a lot of CPU. There is no valid use-case for empty DATA frames without end_of_stream flag.
426428
*
427429
* {@code 0} means no protection should be applied.
@@ -433,6 +435,21 @@ protected B decoderEnforceMaxConsecutiveEmptyDataFrames(int maxConsecutiveEmptyF
433435
return self();
434436
}
435437

438+
/**
439+
* Sets the maximum number RST frames that are allowed per window before
440+
* the connection is closed. This allows to protect against the remote peer flooding us with such frames and
441+
* so use up a lot of CPU.
442+
*
443+
* {@code 0} for any of the parameters means no protection should be applied.
444+
*/
445+
protected B decoderEnforceMaxRstFramesPerWindow(int maxRstFramesPerWindow, int secondsPerWindow) {
446+
enforceNonCodecConstraints("decoderEnforceMaxRstFramesPerWindow");
447+
this.maxRstFramesPerWindow = checkPositiveOrZero(
448+
maxRstFramesPerWindow, "maxRstFramesPerWindow");
449+
this.secondsPerWindow = checkPositiveOrZero(secondsPerWindow, "secondsPerWindow");
450+
return self();
451+
}
452+
436453
/**
437454
* Determine if settings frame should automatically be acknowledged and applied.
438455
* @return this.
@@ -575,6 +592,9 @@ private T buildFromCodec(Http2ConnectionDecoder decoder, Http2ConnectionEncoder
575592
if (maxConsecutiveEmptyDataFrames > 0) {
576593
decoder = new Http2EmptyDataFrameConnectionDecoder(decoder, maxConsecutiveEmptyDataFrames);
577594
}
595+
if (maxRstFramesPerWindow > 0 && secondsPerWindow > 0) {
596+
decoder = new Http2MaxRstFrameDecoder(decoder, maxRstFramesPerWindow, secondsPerWindow);
597+
}
578598
final T handler;
579599
try {
580600
// Call the abstract build method

Diff for: codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodecBuilder.java

+6
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,12 @@ public Http2FrameCodecBuilder decoderEnforceMaxConsecutiveEmptyDataFrames(int ma
194194
return super.decoderEnforceMaxConsecutiveEmptyDataFrames(maxConsecutiveEmptyFrames);
195195
}
196196

197+
@Override
198+
public Http2FrameCodecBuilder decoderEnforceMaxRstFramesPerWindow(
199+
int maxConsecutiveEmptyFrames, int secondsPerWindow) {
200+
return super.decoderEnforceMaxRstFramesPerWindow(maxConsecutiveEmptyFrames, secondsPerWindow);
201+
}
202+
197203
/**
198204
* Build a {@link Http2FrameCodec} object.
199205
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2023 The Netty Project
3+
*
4+
* The Netty Project licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
package io.netty.handler.codec.http2;
17+
18+
import static io.netty.util.internal.ObjectUtil.checkPositive;
19+
20+
21+
/**
22+
* Enforce a limit on the maximum number of RST frames that are allowed per a window
23+
* before the connection will be closed with a GO_AWAY frame.
24+
*/
25+
final class Http2MaxRstFrameDecoder extends DecoratingHttp2ConnectionDecoder {
26+
private final int maxRstFramesPerWindow;
27+
private final int secondsPerWindow;
28+
29+
Http2MaxRstFrameDecoder(Http2ConnectionDecoder delegate, int maxRstFramesPerWindow, int secondsPerWindow) {
30+
super(delegate);
31+
this.maxRstFramesPerWindow = checkPositive(maxRstFramesPerWindow, "maxRstFramesPerWindow");
32+
this.secondsPerWindow = checkPositive(secondsPerWindow, "secondsPerWindow");
33+
}
34+
35+
@Override
36+
public void frameListener(Http2FrameListener listener) {
37+
if (listener != null) {
38+
super.frameListener(new Http2MaxRstFrameListener(listener, maxRstFramesPerWindow, secondsPerWindow));
39+
} else {
40+
super.frameListener(null);
41+
}
42+
}
43+
44+
@Override
45+
public Http2FrameListener frameListener() {
46+
Http2FrameListener frameListener = frameListener0();
47+
// Unwrap the original Http2FrameListener as we add this decoder under the hood.
48+
if (frameListener instanceof Http2MaxRstFrameListener) {
49+
return ((Http2MaxRstFrameListener) frameListener).listener;
50+
}
51+
return frameListener;
52+
}
53+
54+
// Package-private for testing
55+
Http2FrameListener frameListener0() {
56+
return super.frameListener();
57+
}
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2023 The Netty Project
3+
*
4+
* The Netty Project licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
package io.netty.handler.codec.http2;
17+
18+
import io.netty.channel.ChannelHandlerContext;
19+
import io.netty.util.internal.logging.InternalLogger;
20+
import io.netty.util.internal.logging.InternalLoggerFactory;
21+
22+
import java.util.concurrent.TimeUnit;
23+
24+
25+
final class Http2MaxRstFrameListener extends Http2FrameListenerDecorator {
26+
private static final InternalLogger logger = InternalLoggerFactory.getInstance(Http2MaxRstFrameListener.class);
27+
28+
private final long nanosPerWindow;
29+
private final int maxRstFramesPerWindow;
30+
private long lastRstFrameNano = System.nanoTime();
31+
private int receivedRstInWindow;
32+
33+
Http2MaxRstFrameListener(Http2FrameListener listener, int maxRstFramesPerWindow, int secondsPerWindow) {
34+
super(listener);
35+
this.maxRstFramesPerWindow = maxRstFramesPerWindow;
36+
this.nanosPerWindow = TimeUnit.SECONDS.toNanos(secondsPerWindow);
37+
}
38+
39+
@Override
40+
public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) throws Http2Exception {
41+
long currentNano = System.nanoTime();
42+
if (currentNano - lastRstFrameNano >= nanosPerWindow) {
43+
lastRstFrameNano = currentNano;
44+
receivedRstInWindow = 1;
45+
} else {
46+
receivedRstInWindow++;
47+
if (receivedRstInWindow > maxRstFramesPerWindow) {
48+
Http2Exception exception = Http2Exception.connectionError(Http2Error.ENHANCE_YOUR_CALM,
49+
"Maximum number of RST frames reached");
50+
logger.debug("{} Maximum number {} of RST frames reached within {} seconds, " +
51+
"closing connection with {} error", ctx.channel(), maxRstFramesPerWindow,
52+
TimeUnit.NANOSECONDS.toSeconds(nanosPerWindow), exception.error(), exception);
53+
throw exception;
54+
}
55+
}
56+
super.onRstStreamRead(ctx, streamId, errorCode);
57+
}
58+
}

Diff for: codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexCodecBuilder.java

+6
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,12 @@ public Http2MultiplexCodecBuilder decoderEnforceMaxConsecutiveEmptyDataFrames(in
211211
return super.decoderEnforceMaxConsecutiveEmptyDataFrames(maxConsecutiveEmptyFrames);
212212
}
213213

214+
@Override
215+
public Http2MultiplexCodecBuilder decoderEnforceMaxRstFramesPerWindow(
216+
int maxConsecutiveEmptyFrames, int secondsPerWindow) {
217+
return super.decoderEnforceMaxRstFramesPerWindow(maxConsecutiveEmptyFrames, secondsPerWindow);
218+
}
219+
214220
@Override
215221
public Http2MultiplexCodec build() {
216222
Http2FrameWriter frameWriter = this.frameWriter;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2023 The Netty Project
3+
*
4+
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
5+
* "License"); you may not use this file except in compliance with the License. You may obtain a
6+
* copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software distributed under the License
11+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12+
* or implied. See the License for the specific language governing permissions and limitations under
13+
* the License.
14+
*/
15+
package io.netty.handler.codec.http2;
16+
17+
import org.hamcrest.CoreMatchers;
18+
import org.junit.jupiter.api.Test;
19+
import org.mockito.ArgumentCaptor;
20+
import org.mockito.invocation.InvocationOnMock;
21+
import org.mockito.stubbing.Answer;
22+
23+
import static org.junit.jupiter.api.Assertions.assertNull;
24+
import static org.hamcrest.MatcherAssert.assertThat;
25+
import static org.mockito.Mockito.mock;
26+
import static org.mockito.Mockito.verify;
27+
import static org.mockito.Mockito.when;
28+
29+
public abstract class AbstractDecoratingHttp2ConnectionDecoderTest {
30+
31+
protected abstract DecoratingHttp2ConnectionDecoder newDecoder(Http2ConnectionDecoder decoder);
32+
33+
protected abstract Class<? extends Http2FrameListener> delegatingFrameListenerType();
34+
35+
@Test
36+
public void testDecoration() {
37+
Http2ConnectionDecoder delegate = mock(Http2ConnectionDecoder.class);
38+
final ArgumentCaptor<Http2FrameListener> listenerArgumentCaptor =
39+
ArgumentCaptor.forClass(Http2FrameListener.class);
40+
when(delegate.frameListener()).then(new Answer<Http2FrameListener>() {
41+
@Override
42+
public Http2FrameListener answer(InvocationOnMock invocationOnMock) {
43+
return listenerArgumentCaptor.getValue();
44+
}
45+
});
46+
Http2FrameListener listener = mock(Http2FrameListener.class);
47+
DecoratingHttp2ConnectionDecoder decoder = newDecoder(delegate);
48+
decoder.frameListener(listener);
49+
verify(delegate).frameListener(listenerArgumentCaptor.capture());
50+
51+
assertThat(decoder.frameListener(),
52+
CoreMatchers.not(CoreMatchers.instanceOf(delegatingFrameListenerType())));
53+
}
54+
55+
@Test
56+
public void testDecorationWithNull() {
57+
Http2ConnectionDecoder delegate = mock(Http2ConnectionDecoder.class);
58+
59+
DecoratingHttp2ConnectionDecoder decoder = newDecoder(delegate);
60+
decoder.frameListener(null);
61+
assertNull(decoder.frameListener());
62+
}
63+
}

Diff for: codec-http2/src/test/java/io/netty/handler/codec/http2/Http2EmptyDataFrameConnectionDecoderTest.java

+7-39
Original file line numberDiff line numberDiff line change
@@ -14,47 +14,15 @@
1414
*/
1515
package io.netty.handler.codec.http2;
1616

17-
import org.hamcrest.CoreMatchers;
18-
import org.junit.jupiter.api.Test;
19-
import org.mockito.ArgumentCaptor;
20-
import org.mockito.invocation.InvocationOnMock;
21-
import org.mockito.stubbing.Answer;
17+
public class Http2EmptyDataFrameConnectionDecoderTest extends AbstractDecoratingHttp2ConnectionDecoderTest {
2218

23-
import static org.hamcrest.MatcherAssert.assertThat;
24-
import static org.junit.jupiter.api.Assertions.assertNull;
25-
import static org.mockito.Mockito.mock;
26-
import static org.mockito.Mockito.verify;
27-
import static org.mockito.Mockito.when;
28-
29-
public class Http2EmptyDataFrameConnectionDecoderTest {
30-
31-
@Test
32-
public void testDecoration() {
33-
Http2ConnectionDecoder delegate = mock(Http2ConnectionDecoder.class);
34-
final ArgumentCaptor<Http2FrameListener> listenerArgumentCaptor =
35-
ArgumentCaptor.forClass(Http2FrameListener.class);
36-
when(delegate.frameListener()).then(new Answer<Http2FrameListener>() {
37-
@Override
38-
public Http2FrameListener answer(InvocationOnMock invocationOnMock) {
39-
return listenerArgumentCaptor.getValue();
40-
}
41-
});
42-
Http2FrameListener listener = mock(Http2FrameListener.class);
43-
Http2EmptyDataFrameConnectionDecoder decoder = new Http2EmptyDataFrameConnectionDecoder(delegate, 2);
44-
decoder.frameListener(listener);
45-
verify(delegate).frameListener(listenerArgumentCaptor.capture());
46-
47-
assertThat(decoder.frameListener(),
48-
CoreMatchers.not(CoreMatchers.instanceOf(Http2EmptyDataFrameListener.class)));
49-
assertThat(decoder.frameListener0(), CoreMatchers.instanceOf(Http2EmptyDataFrameListener.class));
19+
@Override
20+
protected DecoratingHttp2ConnectionDecoder newDecoder(Http2ConnectionDecoder decoder) {
21+
return new Http2EmptyDataFrameConnectionDecoder(decoder, 2);
5022
}
5123

52-
@Test
53-
public void testDecorationWithNull() {
54-
Http2ConnectionDecoder delegate = mock(Http2ConnectionDecoder.class);
55-
56-
Http2EmptyDataFrameConnectionDecoder decoder = new Http2EmptyDataFrameConnectionDecoder(delegate, 2);
57-
decoder.frameListener(null);
58-
assertNull(decoder.frameListener());
24+
@Override
25+
protected Class<? extends Http2FrameListener> delegatingFrameListenerType() {
26+
return Http2EmptyDataFrameListener.class;
5927
}
6028
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2023 The Netty Project
3+
*
4+
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
5+
* "License"); you may not use this file except in compliance with the License. You may obtain a
6+
* copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software distributed under the License
11+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12+
* or implied. See the License for the specific language governing permissions and limitations under
13+
* the License.
14+
*/
15+
package io.netty.handler.codec.http2;
16+
17+
public class Http2MaxRstFrameConnectionDecoderTest extends AbstractDecoratingHttp2ConnectionDecoderTest {
18+
19+
@Override
20+
protected DecoratingHttp2ConnectionDecoder newDecoder(Http2ConnectionDecoder decoder) {
21+
return new Http2MaxRstFrameDecoder(decoder, 200, 30);
22+
}
23+
24+
@Override
25+
protected Class<? extends Http2FrameListener> delegatingFrameListenerType() {
26+
return Http2MaxRstFrameListener.class;
27+
}
28+
}

0 commit comments

Comments
 (0)