HTTP2: Guard against empty DATA frames (without end_of_stream flag) set (#9461)
Motivation: It is possible for a remote peer to flood the server / client with empty DATA frames (without end_of_stream flag) set and so cause high CPU usage without the possibility to ever hit a limit. We need to guard against this. See CVE-2019-9518 Modifications: - Add a new config option to AbstractHttp2ConnectionBuilder and sub-classes which allows to set the max number of consecutive empty DATA frames (without end_of_stream flag). After this limit is hit we will close the connection. A limit of 10 is used by default. - Add unit tests Result: Guards against CVE-2019-9518
This commit is contained in:
parent
c6c679597f
commit
6283a78e4f
@ -108,6 +108,7 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne
|
||||
private boolean autoAckSettingsFrame = true;
|
||||
private boolean autoAckPingFrame = true;
|
||||
private int maxQueuedControlFrames = Http2CodecUtil.DEFAULT_MAX_QUEUED_CONTROL_FRAMES;
|
||||
private int maxConsecutiveEmptyFrames = 2;
|
||||
|
||||
/**
|
||||
* Sets the {@link Http2Settings} to use for the initial connection settings exchange.
|
||||
@ -407,6 +408,31 @@ 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
|
||||
* 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
|
||||
* 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) {
|
||||
enforceNonCodecConstraints("maxConsecutiveEmptyFrames");
|
||||
this.maxConsecutiveEmptyFrames = ObjectUtil.checkPositiveOrZero(
|
||||
maxConsecutiveEmptyFrames, "maxConsecutiveEmptyFrames");
|
||||
return self();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if settings frame should automatically be acknowledged and applied.
|
||||
* @return this.
|
||||
@ -515,6 +541,10 @@ 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);
|
||||
}
|
||||
final T handler;
|
||||
try {
|
||||
// Call the abstract build method
|
||||
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2019 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:
|
||||
*
|
||||
* http://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.util.internal.ObjectUtil;
|
||||
|
||||
/**
|
||||
* Enforce a limit on the maximum number of consecutive empty DATA frames (without end_of_stream flag) that are allowed
|
||||
* before the connection will be closed.
|
||||
*/
|
||||
final class Http2EmptyDataFrameConnectionDecoder extends DecoratingHttp2ConnectionDecoder {
|
||||
|
||||
private final int maxConsecutiveEmptyFrames;
|
||||
|
||||
Http2EmptyDataFrameConnectionDecoder(Http2ConnectionDecoder delegate, int maxConsecutiveEmptyFrames) {
|
||||
super(delegate);
|
||||
this.maxConsecutiveEmptyFrames = ObjectUtil.checkPositive(
|
||||
maxConsecutiveEmptyFrames, "maxConsecutiveEmptyFrames");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void frameListener(Http2FrameListener listener) {
|
||||
if (listener != null) {
|
||||
super.frameListener(new Http2EmptyDataFrameListener(listener, maxConsecutiveEmptyFrames));
|
||||
} else {
|
||||
super.frameListener(null);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2019 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:
|
||||
*
|
||||
* http://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.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.util.internal.ObjectUtil;
|
||||
|
||||
/**
|
||||
* Enforce a limit on the maximum number of consecutive empty DATA frames (without end_of_stream flag) that are allowed
|
||||
* before the connection will be closed.
|
||||
*/
|
||||
final class Http2EmptyDataFrameListener extends Http2FrameListenerDecorator {
|
||||
private final int maxConsecutiveEmptyFrames;
|
||||
|
||||
private boolean violationDetected;
|
||||
private int emptyDataFrames;
|
||||
|
||||
Http2EmptyDataFrameListener(Http2FrameListener listener, int maxConsecutiveEmptyFrames) {
|
||||
super(listener);
|
||||
this.maxConsecutiveEmptyFrames = ObjectUtil.checkPositive(
|
||||
maxConsecutiveEmptyFrames, "maxConsecutiveEmptyFrames");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream)
|
||||
throws Http2Exception {
|
||||
if (endOfStream || data.isReadable()) {
|
||||
emptyDataFrames = 0;
|
||||
} else if (emptyDataFrames++ == maxConsecutiveEmptyFrames && !violationDetected) {
|
||||
violationDetected = true;
|
||||
throw Http2Exception.connectionError(Http2Error.ENHANCE_YOUR_CALM,
|
||||
"Maximum number %d of empty data frames without end_of_stream flag received",
|
||||
maxConsecutiveEmptyFrames);
|
||||
}
|
||||
|
||||
return super.onDataRead(ctx, streamId, data, padding, endOfStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers,
|
||||
int padding, boolean endStream) throws Http2Exception {
|
||||
emptyDataFrames = 0;
|
||||
super.onHeadersRead(ctx, streamId, headers, padding, endStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency,
|
||||
short weight, boolean exclusive, int padding, boolean endStream) throws Http2Exception {
|
||||
emptyDataFrames = 0;
|
||||
super.onHeadersRead(ctx, streamId, headers, streamDependency, weight, exclusive, padding, endStream);
|
||||
}
|
||||
}
|
@ -167,6 +167,16 @@ public class Http2FrameCodecBuilder extends
|
||||
return super.decoupleCloseAndGoAway(decoupleCloseAndGoAway);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int decoderEnforceMaxConsecutiveEmptyDataFrames() {
|
||||
return super.decoderEnforceMaxConsecutiveEmptyDataFrames();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Http2FrameCodecBuilder decoderEnforceMaxConsecutiveEmptyDataFrames(int maxConsecutiveEmptyFrames) {
|
||||
return super.decoderEnforceMaxConsecutiveEmptyDataFrames(maxConsecutiveEmptyFrames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a {@link Http2FrameCodec} object.
|
||||
*/
|
||||
@ -192,7 +202,10 @@ public class Http2FrameCodecBuilder extends
|
||||
}
|
||||
Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, frameReader,
|
||||
promisedRequestVerifier(), isAutoAckSettingsFrame(), isAutoAckPingFrame());
|
||||
|
||||
int maxConsecutiveEmptyDataFrames = decoderEnforceMaxConsecutiveEmptyDataFrames();
|
||||
if (maxConsecutiveEmptyDataFrames > 0) {
|
||||
decoder = new Http2EmptyDataFrameConnectionDecoder(decoder, maxConsecutiveEmptyDataFrames);
|
||||
}
|
||||
return build(decoder, encoder, initialSettings());
|
||||
}
|
||||
return super.build();
|
||||
|
@ -196,6 +196,16 @@ public class Http2MultiplexCodecBuilder
|
||||
return super.decoupleCloseAndGoAway(decoupleCloseAndGoAway);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int decoderEnforceMaxConsecutiveEmptyDataFrames() {
|
||||
return super.decoderEnforceMaxConsecutiveEmptyDataFrames();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Http2MultiplexCodecBuilder decoderEnforceMaxConsecutiveEmptyDataFrames(int maxConsecutiveEmptyFrames) {
|
||||
return super.decoderEnforceMaxConsecutiveEmptyDataFrames(maxConsecutiveEmptyFrames);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Http2MultiplexCodec build() {
|
||||
Http2FrameWriter frameWriter = this.frameWriter;
|
||||
@ -219,6 +229,11 @@ public class Http2MultiplexCodecBuilder
|
||||
Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, frameReader,
|
||||
promisedRequestVerifier(), isAutoAckSettingsFrame(), isAutoAckPingFrame());
|
||||
|
||||
int maxConsecutiveEmptyDataFrames = decoderEnforceMaxConsecutiveEmptyDataFrames();
|
||||
if (maxConsecutiveEmptyDataFrames > 0) {
|
||||
decoder = new Http2EmptyDataFrameConnectionDecoder(decoder, maxConsecutiveEmptyDataFrames);
|
||||
}
|
||||
|
||||
return build(decoder, encoder, initialSettings());
|
||||
}
|
||||
return super.build();
|
||||
|
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2019 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:
|
||||
*
|
||||
* http://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.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
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.instanceOf(Http2EmptyDataFrameListener.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecorationWithNull() {
|
||||
Http2ConnectionDecoder delegate = mock(Http2ConnectionDecoder.class);
|
||||
|
||||
Http2EmptyDataFrameConnectionDecoder decoder = new Http2EmptyDataFrameConnectionDecoder(delegate, 2);
|
||||
decoder.frameListener(null);
|
||||
assertNull(decoder.frameListener());
|
||||
}
|
||||
}
|
@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright 2019 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:
|
||||
*
|
||||
* http://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.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.eq;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.MockitoAnnotations.initMocks;
|
||||
|
||||
public class Http2EmptyDataFrameListenerTest {
|
||||
|
||||
@Mock
|
||||
private Http2FrameListener frameListener;
|
||||
@Mock
|
||||
private ChannelHandlerContext ctx;
|
||||
|
||||
@Mock
|
||||
private ByteBuf nonEmpty;
|
||||
|
||||
private Http2EmptyDataFrameListener listener;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
initMocks(this);
|
||||
when(nonEmpty.isReadable()).thenReturn(true);
|
||||
listener = new Http2EmptyDataFrameListener(frameListener, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyDataFrames() throws Http2Exception {
|
||||
listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
|
||||
listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
|
||||
|
||||
try {
|
||||
listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
|
||||
fail();
|
||||
} catch (Http2Exception expected) {
|
||||
// expected
|
||||
}
|
||||
verify(frameListener, times(2)).onDataRead(eq(ctx), eq(1), any(ByteBuf.class), eq(0), eq(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyDataFramesWithNonEmptyInBetween() throws Http2Exception {
|
||||
Http2EmptyDataFrameListener listener = new Http2EmptyDataFrameListener(frameListener, 2);
|
||||
listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
|
||||
listener.onDataRead(ctx, 1, nonEmpty, 0, false);
|
||||
|
||||
listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
|
||||
listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
|
||||
|
||||
try {
|
||||
listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
|
||||
fail();
|
||||
} catch (Http2Exception expected) {
|
||||
// expected
|
||||
}
|
||||
verify(frameListener, times(4)).onDataRead(eq(ctx), eq(1), any(ByteBuf.class), eq(0), eq(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyDataFramesWithEndOfStreamInBetween() throws Http2Exception {
|
||||
Http2EmptyDataFrameListener listener = new Http2EmptyDataFrameListener(frameListener, 2);
|
||||
listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
|
||||
listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, true);
|
||||
|
||||
listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
|
||||
listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
|
||||
|
||||
try {
|
||||
listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
|
||||
fail();
|
||||
} catch (Http2Exception expected) {
|
||||
// expected
|
||||
}
|
||||
verify(frameListener, times(1)).onDataRead(eq(ctx), eq(1), any(ByteBuf.class), eq(0), eq(true));
|
||||
verify(frameListener, times(3)).onDataRead(eq(ctx), eq(1), any(ByteBuf.class), eq(0), eq(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyDataFramesWithHeaderFrameInBetween() throws Http2Exception {
|
||||
Http2EmptyDataFrameListener listener = new Http2EmptyDataFrameListener(frameListener, 2);
|
||||
listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
|
||||
listener.onHeadersRead(ctx, 1, EmptyHttp2Headers.INSTANCE, 0, true);
|
||||
|
||||
listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
|
||||
listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
|
||||
|
||||
try {
|
||||
listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
|
||||
fail();
|
||||
} catch (Http2Exception expected) {
|
||||
// expected
|
||||
}
|
||||
|
||||
verify(frameListener, times(1)).onHeadersRead(eq(ctx), eq(1), eq(EmptyHttp2Headers.INSTANCE), eq(0), eq(true));
|
||||
verify(frameListener, times(3)).onDataRead(eq(ctx), eq(1), any(ByteBuf.class), eq(0), eq(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyDataFramesWithHeaderFrameInBetween2() throws Http2Exception {
|
||||
Http2EmptyDataFrameListener listener = new Http2EmptyDataFrameListener(frameListener, 2);
|
||||
listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
|
||||
listener.onHeadersRead(ctx, 1, EmptyHttp2Headers.INSTANCE, 0, (short) 0, false, 0, true);
|
||||
|
||||
listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
|
||||
listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
|
||||
|
||||
try {
|
||||
listener.onDataRead(ctx, 1, Unpooled.EMPTY_BUFFER, 0, false);
|
||||
fail();
|
||||
} catch (Http2Exception expected) {
|
||||
// expected
|
||||
}
|
||||
|
||||
verify(frameListener, times(1)).onHeadersRead(eq(ctx), eq(1),
|
||||
eq(EmptyHttp2Headers.INSTANCE), eq(0), eq((short) 0), eq(false), eq(0), eq(true));
|
||||
verify(frameListener, times(3)).onDataRead(eq(ctx), eq(1), any(ByteBuf.class), eq(0), eq(false));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user