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

Limit the number of Continuation frames per HTTP2 Headers #13969

Open
wants to merge 1 commit into
base: 4.1
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne
private int maxQueuedControlFrames = Http2CodecUtil.DEFAULT_MAX_QUEUED_CONTROL_FRAMES;
private int maxConsecutiveEmptyFrames = 2;
private Integer maxRstFramesPerWindow;
private int maxConsecutiveContinuationsFrames = 16;

private int secondsPerWindow = 30;

/**
Expand Down Expand Up @@ -452,6 +454,29 @@ protected B decoderEnforceMaxRstFramesPerWindow(int maxRstFramesPerWindow, int s
return self();
}

/**
* Returns the maximum number of consecutive CONTINUATION frames that are allowed before
* the connection is closed. This allows to protect against the remote peer flooding us with such frames.
*
* {@code 0} means no protection is in place.
*/
protected int decoderEnforceMaxConsecutiveContinuationsFrames() {
return maxConsecutiveEmptyFrames;
}

/**
* Sets the maximum number of consecutive CONTINUATION frames that are allowed before
* the connection is closed. This allows to protect against the remote peer flooding us with such frames.
*
* {@code 0} means no protection should be applied.
*/
protected B decoderEnforceMaxConsecutiveContinuationsFrames(int maxConsecutiveContinuationsFrames) {
enforceNonCodecConstraints("maxConsecutiveContinuationsFrames");
this.maxConsecutiveContinuationsFrames = checkPositiveOrZero(
maxConsecutiveContinuationsFrames, "maxConsecutiveContinuationsFrames");
return self();
}

/**
* Determine if settings frame should automatically be acknowledged and applied.
* @return this.
Expand Down Expand Up @@ -557,7 +582,7 @@ private T buildFromConnection(Http2Connection connection) {
Long maxHeaderListSize = initialSettings.maxHeaderListSize();
Http2FrameReader reader = new DefaultHttp2FrameReader(new DefaultHttp2HeadersDecoder(isValidateHeaders(),
maxHeaderListSize == null ? DEFAULT_HEADER_LIST_SIZE : maxHeaderListSize,
/* initialHuffmanDecodeCapacity= */ -1));
/* initialHuffmanDecodeCapacity= */ -1), maxConsecutiveContinuationsFrames);
Http2FrameWriter writer = encoderIgnoreMaxHeaderListSize == null ?
new DefaultHttp2FrameWriter(headerSensitivityDetector()) :
new DefaultHttp2FrameWriter(headerSensitivityDetector(), encoderIgnoreMaxHeaderListSize);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http2.Http2FrameReader.Configuration;
import io.netty.util.internal.ObjectUtil;
import io.netty.util.internal.PlatformDependent;
import io.netty.util.internal.UnstableApi;

Expand All @@ -32,6 +33,7 @@
import static io.netty.handler.codec.http2.Http2CodecUtil.headerListSizeExceeded;
import static io.netty.handler.codec.http2.Http2CodecUtil.isMaxFrameSizeValid;
import static io.netty.handler.codec.http2.Http2CodecUtil.readUnsignedInt;
import static io.netty.handler.codec.http2.Http2Error.ENHANCE_YOUR_CALM;
import static io.netty.handler.codec.http2.Http2Error.FLOW_CONTROL_ERROR;
import static io.netty.handler.codec.http2.Http2Error.FRAME_SIZE_ERROR;
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
Expand Down Expand Up @@ -69,7 +71,8 @@ public class DefaultHttp2FrameReader implements Http2FrameReader, Http2FrameSize
private Http2Flags flags;
private int payloadLength;
private HeadersContinuation headersContinuation;
private int maxFrameSize;
private int maxFrameSize = DEFAULT_MAX_FRAME_SIZE;
private final int maxConsecutiveContinuationsFrames;

/**
* Create a new instance.
Expand All @@ -90,8 +93,13 @@ public DefaultHttp2FrameReader(boolean validateHeaders) {
}

public DefaultHttp2FrameReader(Http2HeadersDecoder headersDecoder) {
this.headersDecoder = headersDecoder;
maxFrameSize = DEFAULT_MAX_FRAME_SIZE;
this(headersDecoder, 16);
}

public DefaultHttp2FrameReader(Http2HeadersDecoder headersDecoder, int maxConsecutiveContinuationsFrames) {
this.headersDecoder = ObjectUtil.checkNotNull(headersDecoder, "headersDecoder");
this.maxConsecutiveContinuationsFrames = ObjectUtil.checkPositiveOrZero(
maxConsecutiveContinuationsFrames, "maxConsecutiveContinuationsFrames");
}

@Override
Expand Down Expand Up @@ -392,6 +400,12 @@ private void verifyContinuationFrame() throws Http2Exception {
throw connectionError(PROTOCOL_ERROR, "Continuation stream ID does not match pending headers. "
+ "Expected %d, but received %d.", headersContinuation.getStreamId(), streamId);
}

if (headersContinuation.numFragments() >= maxConsecutiveContinuationsFrames) {
throw connectionError(ENHANCE_YOUR_CALM,
"Number of consecutive continuations frames %d exceeds maximum: %d",
headersContinuation.numFragments(), maxConsecutiveContinuationsFrames);
}
}

private void verifyUnknownFrame() throws Http2Exception {
Expand Down Expand Up @@ -652,6 +666,15 @@ private abstract class HeadersContinuation {
*/
abstract int getStreamId();

/**
* Return the number of fragments that were used so far.
*
* @return the number of fragments
*/
final int numFragments() {
return builder.numFragments();
}

/**
* Processes the next fragment for the current header block.
*
Expand Down Expand Up @@ -680,6 +703,7 @@ final void close() {
*/
protected class HeadersBlockBuilder {
private ByteBuf headerBlock;
private int numFragments;

/**
* The local header size maximum has been exceeded while accumulating bytes.
Expand All @@ -690,6 +714,15 @@ private void headerSizeExceeded() throws Http2Exception {
headerListSizeExceeded(headersDecoder.configuration().maxHeaderListSizeGoAway());
}

/**
* Return the number of fragments that was used so far.
*
* @return number of fragments.
*/
int numFragments() {
return numFragments;
}

/**
* Adds a fragment to the block.
*
Expand All @@ -701,6 +734,8 @@ private void headerSizeExceeded() throws Http2Exception {
*/
final void addFragment(ByteBuf fragment, int len, ByteBufAllocator alloc,
boolean endOfHeaders) throws Http2Exception {
numFragments++;

if (headerBlock == null) {
if (len > headersDecoder.configuration().maxHeaderListSizeGoAway()) {
headerSizeExceeded();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,17 @@ public Http2FrameCodecBuilder decoderEnforceMaxRstFramesPerWindow(
return super.decoderEnforceMaxRstFramesPerWindow(maxRstFramesPerWindow, secondsPerWindow);
}

@Override
public int decoderEnforceMaxConsecutiveContinuationsFrames() {
return super.decoderEnforceMaxConsecutiveContinuationsFrames();
}

@Override
public Http2FrameCodecBuilder decoderEnforceMaxConsecutiveContinuationsFrames(
int maxConsecutiveContinuationsFrames) {
return super.decoderEnforceMaxConsecutiveContinuationsFrames(maxConsecutiveContinuationsFrames);
}

/**
* Build a {@link Http2FrameCodec} object.
*/
Expand All @@ -213,7 +224,8 @@ public Http2FrameCodec build() {
Long maxHeaderListSize = initialSettings().maxHeaderListSize();
Http2FrameReader frameReader = new DefaultHttp2FrameReader(maxHeaderListSize == null ?
new DefaultHttp2HeadersDecoder(isValidateHeaders()) :
new DefaultHttp2HeadersDecoder(isValidateHeaders(), maxHeaderListSize));
new DefaultHttp2HeadersDecoder(isValidateHeaders(), maxHeaderListSize),
decoderEnforceMaxConsecutiveContinuationsFrames());

if (frameLogger() != null) {
frameWriter = new Http2OutboundFrameLogger(frameWriter, frameLogger());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,17 @@ public Http2MultiplexCodecBuilder decoderEnforceMaxRstFramesPerWindow(
return super.decoderEnforceMaxRstFramesPerWindow(maxRstFramesPerWindow, secondsPerWindow);
}

@Override
public int decoderEnforceMaxConsecutiveContinuationsFrames() {
return super.decoderEnforceMaxConsecutiveContinuationsFrames();
}

@Override
public Http2MultiplexCodecBuilder decoderEnforceMaxConsecutiveContinuationsFrames(
int maxConsecutiveContinuationsFrames) {
return super.decoderEnforceMaxConsecutiveContinuationsFrames(maxConsecutiveContinuationsFrames);
}

@Override
public Http2MultiplexCodec build() {
Http2FrameWriter frameWriter = this.frameWriter;
Expand All @@ -227,7 +238,8 @@ public Http2MultiplexCodec build() {
Long maxHeaderListSize = initialSettings().maxHeaderListSize();
Http2FrameReader frameReader = new DefaultHttp2FrameReader(maxHeaderListSize == null ?
new DefaultHttp2HeadersDecoder(isValidateHeaders()) :
new DefaultHttp2HeadersDecoder(isValidateHeaders(), maxHeaderListSize));
new DefaultHttp2HeadersDecoder(isValidateHeaders(), maxHeaderListSize),
decoderEnforceMaxConsecutiveContinuationsFrames());

if (frameLogger() != null) {
frameWriter = new Http2OutboundFrameLogger(frameWriter, frameLogger());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,39 @@ public void readHeaderFrameAndContinuationFrame() throws Http2Exception {
}
}

@Test
public void readHeaderFrameAndContinuationFrameExceedMax() throws Http2Exception {
frameReader = new DefaultHttp2FrameReader(new DefaultHttp2HeadersDecoder(true), 2);
final int streamId = 1;

final ByteBuf input = Unpooled.buffer();
try {
Http2Headers headers = new DefaultHttp2Headers()
.authority("foo")
.method("get")
.path("/")
.scheme("https");
writeHeaderFrame(input, streamId, headers,
new Http2Flags().endOfHeaders(false).endOfStream(true));
writeContinuationFrame(input, streamId, new DefaultHttp2Headers().add("foo", "bar"),
new Http2Flags().endOfHeaders(false));
writeContinuationFrame(input, streamId, new DefaultHttp2Headers().add("foo2", "bar2"),
new Http2Flags().endOfHeaders(false));
writeContinuationFrame(input, streamId, new DefaultHttp2Headers().add("foo3", "bar3"),
new Http2Flags().endOfHeaders(false));

Http2Exception ex = assertThrows(Http2Exception.class, new Executable() {
@Override
public void execute() throws Throwable {
frameReader.readFrame(ctx, input, listener);
}
});
assertEquals(Http2Error.ENHANCE_YOUR_CALM, ex.error());
} finally {
input.release();
}
}

@Test
public void readUnknownFrame() throws Http2Exception {
ByteBuf input = Unpooled.buffer();
Expand Down