Skip to content

Commit

Permalink
Merge pull request from GHSA-xpw8-rcwv-8f8p
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
normanmaurer committed Oct 10, 2023
1 parent 4911448 commit 58f75f6
Show file tree
Hide file tree
Showing 9 changed files with 316 additions and 41 deletions.
Expand Up @@ -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.
Expand Down Expand Up @@ -410,7 +412,7 @@ protected Http2PromisedRequestVerifier 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.
Expand All @@ -421,7 +423,7 @@ protected int decoderEnforceMaxConsecutiveEmptyDataFrames() {

/**
* 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.
Expand All @@ -433,6 +435,21 @@ protected B decoderEnforceMaxConsecutiveEmptyDataFrames(int maxConsecutiveEmptyF
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.
Expand Down Expand Up @@ -575,6 +592,9 @@ private T buildFromCodec(Http2ConnectionDecoder decoder, Http2ConnectionEncoder
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
Expand Down
Expand Up @@ -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.
*/
Expand Down
@@ -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();
}
}
@@ -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);
}
}
Expand Up @@ -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;
Expand Down
@@ -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());
}
}
Expand Up @@ -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;
}
}
@@ -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;
}
}

0 comments on commit 58f75f6

Please sign in to comment.