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:
Norman Maurer 2019-08-13 19:07:10 +02:00
parent c6c679597f
commit 6283a78e4f
7 changed files with 365 additions and 1 deletions

View File

@ -108,6 +108,7 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne
private boolean autoAckSettingsFrame = true; private boolean autoAckSettingsFrame = true;
private boolean autoAckPingFrame = true; private boolean autoAckPingFrame = true;
private int maxQueuedControlFrames = Http2CodecUtil.DEFAULT_MAX_QUEUED_CONTROL_FRAMES; 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. * 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; 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. * Determine if settings frame should automatically be acknowledged and applied.
* @return this. * @return this.
@ -515,6 +541,10 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne
} }
private T buildFromCodec(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder) { private T buildFromCodec(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder) {
int maxConsecutiveEmptyDataFrames = decoderEnforceMaxConsecutiveEmptyDataFrames();
if (maxConsecutiveEmptyDataFrames > 0) {
decoder = new Http2EmptyDataFrameConnectionDecoder(decoder, maxConsecutiveEmptyDataFrames);
}
final T handler; final T handler;
try { try {
// Call the abstract build method // Call the abstract build method

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -167,6 +167,16 @@ public class Http2FrameCodecBuilder extends
return super.decoupleCloseAndGoAway(decoupleCloseAndGoAway); 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. * Build a {@link Http2FrameCodec} object.
*/ */
@ -192,7 +202,10 @@ public class Http2FrameCodecBuilder extends
} }
Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, frameReader, Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, frameReader,
promisedRequestVerifier(), isAutoAckSettingsFrame(), isAutoAckPingFrame()); promisedRequestVerifier(), isAutoAckSettingsFrame(), isAutoAckPingFrame());
int maxConsecutiveEmptyDataFrames = decoderEnforceMaxConsecutiveEmptyDataFrames();
if (maxConsecutiveEmptyDataFrames > 0) {
decoder = new Http2EmptyDataFrameConnectionDecoder(decoder, maxConsecutiveEmptyDataFrames);
}
return build(decoder, encoder, initialSettings()); return build(decoder, encoder, initialSettings());
} }
return super.build(); return super.build();

View File

@ -196,6 +196,16 @@ public class Http2MultiplexCodecBuilder
return super.decoupleCloseAndGoAway(decoupleCloseAndGoAway); return super.decoupleCloseAndGoAway(decoupleCloseAndGoAway);
} }
@Override
public int decoderEnforceMaxConsecutiveEmptyDataFrames() {
return super.decoderEnforceMaxConsecutiveEmptyDataFrames();
}
@Override
public Http2MultiplexCodecBuilder decoderEnforceMaxConsecutiveEmptyDataFrames(int maxConsecutiveEmptyFrames) {
return super.decoderEnforceMaxConsecutiveEmptyDataFrames(maxConsecutiveEmptyFrames);
}
@Override @Override
public Http2MultiplexCodec build() { public Http2MultiplexCodec build() {
Http2FrameWriter frameWriter = this.frameWriter; Http2FrameWriter frameWriter = this.frameWriter;
@ -219,6 +229,11 @@ public class Http2MultiplexCodecBuilder
Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, frameReader, Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, frameReader,
promisedRequestVerifier(), isAutoAckSettingsFrame(), isAutoAckPingFrame()); promisedRequestVerifier(), isAutoAckSettingsFrame(), isAutoAckPingFrame());
int maxConsecutiveEmptyDataFrames = decoderEnforceMaxConsecutiveEmptyDataFrames();
if (maxConsecutiveEmptyDataFrames > 0) {
decoder = new Http2EmptyDataFrameConnectionDecoder(decoder, maxConsecutiveEmptyDataFrames);
}
return build(decoder, encoder, initialSettings()); return build(decoder, encoder, initialSettings());
} }
return super.build(); return super.build();

View File

@ -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());
}
}

View File

@ -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));
}
}