Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP2: Guard against empty DATA frames (without end_of_stream flag) set #9461

Merged
merged 1 commit into from Aug 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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());
}
}