Skip to content

Commit

Permalink
HTTP2: Guard against empty DATA frames (without end_of_stream flag) s…
Browse files Browse the repository at this point in the history
…et (#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
  • Loading branch information
normanmaurer committed Aug 13, 2019
1 parent cecb46a commit 7003dbd
Show file tree
Hide file tree
Showing 7 changed files with 365 additions and 1 deletion.
Expand Up @@ -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.
Expand Down Expand Up @@ -407,6 +408,31 @@ protected Http2PromisedRequestVerifier 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.
* @return this.
Expand Down Expand Up @@ -515,6 +541,10 @@ private T buildFromConnection(Http2Connection connection) {
}

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
Expand Down
@@ -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);
}
}
Expand Up @@ -167,6 +167,16 @@ public Http2FrameCodecBuilder decoupleCloseAndGoAway(boolean decoupleCloseAndGoA
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.
*/
Expand All @@ -192,7 +202,10 @@ public Http2FrameCodec build() {
}
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();
Expand Down
Expand Up @@ -196,6 +196,16 @@ public Http2MultiplexCodecBuilder decoupleCloseAndGoAway(boolean decoupleCloseAn
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;
Expand All @@ -219,6 +229,11 @@ public Http2MultiplexCodec build() {
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();
Expand Down
@@ -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 comments on commit 7003dbd

Please sign in to comment.