Skip to content

Commit aae944a

Browse files
netty-project-botnormanmaurerbryce-andersonchrisvest
authored
Auto-port 4.2: Limit the number of Continuation frames per HTTP2 Headers (#16536)
Auto-port of #13969 to 4.2 Cherry-picked commit: 9f47a7b --- Motivation: We should limit the number of continuation frames that the remote peer is allowed to sent per headers. Modifications: - Limit the number of continuation frames by default to 16 and allow the user to change this. - Add unit test Result: Do some more validations to guard against resource usage Co-authored-by: Norman Maurer <norman_maurer@apple.com> Co-authored-by: Bryce Anderson <bl_anderson@apple.com> Co-authored-by: Chris Vest <mr.chrisvest@gmail.com>
1 parent 6001499 commit aae944a

6 files changed

Lines changed: 150 additions & 8 deletions

File tree

codec-http2/src/main/java/io/netty/handler/codec/http2/AbstractHttp2ConnectionHandlerBuilder.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
* License for the specific language governing permissions and limitations
1414
* under the License.
1515
*/
16-
1716
package io.netty.handler.codec.http2;
1817

1918
import io.netty.channel.Channel;
@@ -113,6 +112,8 @@ public abstract class AbstractHttp2ConnectionHandlerBuilder<T extends Http2Conne
113112
private int maxDecodedRstFramesSecondsPerWindow = 30;
114113
private Integer maxEncodedRstFramesPerWindow;
115114
private int maxEncodedRstFramesSecondsPerWindow = 30;
115+
private int maxSmallContinuationFrames = Http2CodecUtil.DEFAULT_MAX_SMALL_CONTINUATION_FRAME;
116+
116117
/**
117118
* Sets the {@link Http2Settings} to use for the initial connection settings exchange.
118119
*/
@@ -466,6 +467,30 @@ protected B encoderEnforceMaxRstFramesPerWindow(int maxRstFramesPerWindow, int s
466467
return self();
467468
}
468469

470+
/**
471+
* Returns the maximum number of small CONTINUATION frames per HEADERS block that are allowed
472+
* before the connection is closed. Small is defined as 8 KiB, half the minimum allowed HTTP2 frame size.
473+
* This setting is to protect against the remote peer flooding us with such frames.
474+
*
475+
* {@code 0} means no protection is in place.
476+
*/
477+
protected int decoderEnforceMaxSmallContinuationFrames() {
478+
return maxSmallContinuationFrames;
479+
}
480+
481+
/**
482+
* Returns the maximum number of small CONTINUATION frames per HEADERS block that are allowed
483+
* before the connection is closed. Small is defined as 8 KiB, half the minimum allowed HTTP2 frame size.
484+
* This setting is to protect against the remote peer flooding us with such frames.
485+
* {@code 0} means no protection should be applied.
486+
*/
487+
protected B decoderEnforceMaxSmallContinuationFrames(int maxSmallContinuationFrames) {
488+
enforceNonCodecConstraints("maxSmallContinuationFrames");
489+
this.maxSmallContinuationFrames = checkPositiveOrZero(
490+
maxSmallContinuationFrames, "maxSmallContinuationFrames");
491+
return self();
492+
}
493+
469494
/**
470495
* Determine if settings frame should automatically be acknowledged and applied.
471496
* @return this.
@@ -571,7 +596,7 @@ private T buildFromConnection(Http2Connection connection) {
571596
Long maxHeaderListSize = initialSettings.maxHeaderListSize();
572597
Http2FrameReader reader = new DefaultHttp2FrameReader(new DefaultHttp2HeadersDecoder(isValidateHeaders(),
573598
maxHeaderListSize == null ? DEFAULT_HEADER_LIST_SIZE : maxHeaderListSize,
574-
/* initialHuffmanDecodeCapacity= */ -1));
599+
/* initialHuffmanDecodeCapacity= */ -1), maxSmallContinuationFrames);
575600
Http2FrameWriter writer = encoderIgnoreMaxHeaderListSize == null ?
576601
new DefaultHttp2FrameWriter(headerSensitivityDetector()) :
577602
new DefaultHttp2FrameWriter(headerSensitivityDetector(), encoderIgnoreMaxHeaderListSize);

codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2FrameReader.java

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,22 @@
1818
import io.netty.buffer.ByteBufAllocator;
1919
import io.netty.channel.ChannelHandlerContext;
2020
import io.netty.handler.codec.http2.Http2FrameReader.Configuration;
21+
import io.netty.util.internal.ObjectUtil;
2122
import io.netty.util.internal.PlatformDependent;
2223

2324
import static io.netty.handler.codec.http2.Http2CodecUtil.CONNECTION_STREAM_ID;
2425
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_MAX_FRAME_SIZE;
2526
import static io.netty.handler.codec.http2.Http2CodecUtil.FRAME_HEADER_LENGTH;
2627
import static io.netty.handler.codec.http2.Http2CodecUtil.INT_FIELD_LENGTH;
28+
import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_FRAME_SIZE_LOWER_BOUND;
2729
import static io.netty.handler.codec.http2.Http2CodecUtil.PING_FRAME_PAYLOAD_LENGTH;
2830
import static io.netty.handler.codec.http2.Http2CodecUtil.PRIORITY_ENTRY_LENGTH;
2931
import static io.netty.handler.codec.http2.Http2CodecUtil.SETTINGS_INITIAL_WINDOW_SIZE;
3032
import static io.netty.handler.codec.http2.Http2CodecUtil.SETTING_ENTRY_LENGTH;
3133
import static io.netty.handler.codec.http2.Http2CodecUtil.headerListSizeExceeded;
3234
import static io.netty.handler.codec.http2.Http2CodecUtil.isMaxFrameSizeValid;
3335
import static io.netty.handler.codec.http2.Http2CodecUtil.readUnsignedInt;
36+
import static io.netty.handler.codec.http2.Http2Error.ENHANCE_YOUR_CALM;
3437
import static io.netty.handler.codec.http2.Http2Error.FLOW_CONTROL_ERROR;
3538
import static io.netty.handler.codec.http2.Http2Error.FRAME_SIZE_ERROR;
3639
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
@@ -51,6 +54,7 @@
5154
* A {@link Http2FrameReader} that supports all frame types defined by the HTTP/2 specification.
5255
*/
5356
public class DefaultHttp2FrameReader implements Http2FrameReader, Http2FrameSizePolicy, Configuration {
57+
private static final int FRAGMENT_THRESHOLD = MAX_FRAME_SIZE_LOWER_BOUND / 2;
5458
private final Http2HeadersDecoder headersDecoder;
5559

5660
/**
@@ -67,7 +71,8 @@ public class DefaultHttp2FrameReader implements Http2FrameReader, Http2FrameSize
6771
private Http2Flags flags;
6872
private int payloadLength;
6973
private HeadersContinuation headersContinuation;
70-
private int maxFrameSize;
74+
private int maxFrameSize = DEFAULT_MAX_FRAME_SIZE;
75+
private final int maxSmallContinuationFrames;
7176

7277
/**
7378
* Create a new instance.
@@ -88,8 +93,13 @@ public DefaultHttp2FrameReader(boolean validateHeaders) {
8893
}
8994

9095
public DefaultHttp2FrameReader(Http2HeadersDecoder headersDecoder) {
91-
this.headersDecoder = headersDecoder;
92-
maxFrameSize = DEFAULT_MAX_FRAME_SIZE;
96+
this(headersDecoder, Http2CodecUtil.DEFAULT_MAX_SMALL_CONTINUATION_FRAME);
97+
}
98+
99+
public DefaultHttp2FrameReader(Http2HeadersDecoder headersDecoder, int maxSmallContinuationFrames) {
100+
this.headersDecoder = ObjectUtil.checkNotNull(headersDecoder, "headersDecoder");
101+
this.maxSmallContinuationFrames = ObjectUtil.checkPositiveOrZero(
102+
maxSmallContinuationFrames, "maxSmallContinuationFrames");
93103
}
94104

95105
@Override
@@ -390,6 +400,12 @@ private void verifyContinuationFrame() throws Http2Exception {
390400
throw connectionError(PROTOCOL_ERROR, "Continuation stream ID does not match pending headers. "
391401
+ "Expected %d, but received %d.", headersContinuation.getStreamId(), streamId);
392402
}
403+
404+
if (headersContinuation.numSmallFragments() >= maxSmallContinuationFrames) {
405+
throw connectionError(ENHANCE_YOUR_CALM,
406+
"Number of small consecutive continuations frames %d exceeds maximum: %d",
407+
headersContinuation.numSmallFragments(), maxSmallContinuationFrames);
408+
}
393409
}
394410

395411
private void verifyUnknownFrame() throws Http2Exception {
@@ -645,6 +661,15 @@ private abstract class HeadersContinuation {
645661
*/
646662
abstract int getStreamId();
647663

664+
/**
665+
* Return the number of fragments that were used so far.
666+
*
667+
* @return the number of fragments
668+
*/
669+
final int numSmallFragments() {
670+
return builder.numSmallFragments();
671+
}
672+
648673
/**
649674
* Processes the next fragment for the current header block.
650675
*
@@ -673,6 +698,7 @@ final void close() {
673698
*/
674699
protected class HeadersBlockBuilder {
675700
private ByteBuf headerBlock;
701+
private int numSmallFragments;
676702

677703
/**
678704
* The local header size maximum has been exceeded while accumulating bytes.
@@ -683,6 +709,15 @@ private void headerSizeExceeded() throws Http2Exception {
683709
headerListSizeExceeded(headersDecoder.configuration().maxHeaderListSizeGoAway());
684710
}
685711

712+
/**
713+
* Return the number of fragments that was used so far.
714+
*
715+
* @return number of fragments.
716+
*/
717+
int numSmallFragments() {
718+
return numSmallFragments;
719+
}
720+
686721
/**
687722
* Adds a fragment to the block.
688723
*
@@ -694,6 +729,11 @@ private void headerSizeExceeded() throws Http2Exception {
694729
*/
695730
final void addFragment(ByteBuf fragment, int len, ByteBufAllocator alloc,
696731
boolean endOfHeaders) throws Http2Exception {
732+
if (maxSmallContinuationFrames > 0 && !endOfHeaders && len < FRAGMENT_THRESHOLD) {
733+
// Only count of the fragment is not the end of header and if its < 8kb.
734+
numSmallFragments++;
735+
}
736+
697737
if (headerBlock == null) {
698738
if (len > headersDecoder.configuration().maxHeaderListSizeGoAway()) {
699739
headerSizeExceeded();

codec-http2/src/main/java/io/netty/handler/codec/http2/Http2CodecUtil.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ public final class Http2CodecUtil {
117117
public static final int SMALLEST_MAX_CONCURRENT_STREAMS = 100;
118118
static final int DEFAULT_MAX_RESERVED_STREAMS = SMALLEST_MAX_CONCURRENT_STREAMS;
119119
static final int DEFAULT_MIN_ALLOCATION_CHUNK = 1024;
120-
120+
static final int DEFAULT_MAX_SMALL_CONTINUATION_FRAME = 16;
121121
/**
122122
* Calculate the threshold in bytes which should trigger a {@code GO_AWAY} if a set of headers exceeds this amount.
123123
* @param maxHeaderListSize

codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodecBuilder.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,17 @@ public Http2FrameCodecBuilder encoderEnforceMaxRstFramesPerWindow(
203203
return super.encoderEnforceMaxRstFramesPerWindow(maxRstFramesPerWindow, secondsPerWindow);
204204
}
205205

206+
@Override
207+
public int decoderEnforceMaxSmallContinuationFrames() {
208+
return super.decoderEnforceMaxSmallContinuationFrames();
209+
}
210+
211+
@Override
212+
public Http2FrameCodecBuilder decoderEnforceMaxSmallContinuationFrames(
213+
int maxConsecutiveContinuationsFrames) {
214+
return super.decoderEnforceMaxSmallContinuationFrames(maxConsecutiveContinuationsFrames);
215+
}
216+
206217
/**
207218
* Build a {@link Http2FrameCodec} object.
208219
*/
@@ -216,7 +227,8 @@ public Http2FrameCodec build() {
216227
Long maxHeaderListSize = initialSettings().maxHeaderListSize();
217228
Http2FrameReader frameReader = new DefaultHttp2FrameReader(maxHeaderListSize == null ?
218229
new DefaultHttp2HeadersDecoder(isValidateHeaders()) :
219-
new DefaultHttp2HeadersDecoder(isValidateHeaders(), maxHeaderListSize));
230+
new DefaultHttp2HeadersDecoder(isValidateHeaders(), maxHeaderListSize),
231+
decoderEnforceMaxSmallContinuationFrames());
220232

221233
if (frameLogger() != null) {
222234
frameWriter = new Http2OutboundFrameLogger(frameWriter, frameLogger());

codec-http2/src/main/java/io/netty/handler/codec/http2/Http2MultiplexCodecBuilder.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,17 @@ public Http2MultiplexCodecBuilder encoderEnforceMaxRstFramesPerWindow(
221221
return super.encoderEnforceMaxRstFramesPerWindow(maxRstFramesPerWindow, secondsPerWindow);
222222
}
223223

224+
@Override
225+
public int decoderEnforceMaxSmallContinuationFrames() {
226+
return super.decoderEnforceMaxSmallContinuationFrames();
227+
}
228+
229+
@Override
230+
public Http2MultiplexCodecBuilder decoderEnforceMaxSmallContinuationFrames(
231+
int maxConsecutiveContinuationsFrames) {
232+
return super.decoderEnforceMaxSmallContinuationFrames(maxConsecutiveContinuationsFrames);
233+
}
234+
224235
@Override
225236
public Http2MultiplexCodec build() {
226237
Http2FrameWriter frameWriter = this.frameWriter;
@@ -231,7 +242,8 @@ public Http2MultiplexCodec build() {
231242
Long maxHeaderListSize = initialSettings().maxHeaderListSize();
232243
Http2FrameReader frameReader = new DefaultHttp2FrameReader(maxHeaderListSize == null ?
233244
new DefaultHttp2HeadersDecoder(isValidateHeaders()) :
234-
new DefaultHttp2HeadersDecoder(isValidateHeaders(), maxHeaderListSize));
245+
new DefaultHttp2HeadersDecoder(isValidateHeaders(), maxHeaderListSize),
246+
decoderEnforceMaxSmallContinuationFrames());
235247

236248
if (frameLogger() != null) {
237249
frameWriter = new Http2OutboundFrameLogger(frameWriter, frameLogger());

codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2FrameReaderTest.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,59 @@ public void readHeaderFrameAndContinuationFrame() throws Http2Exception {
109109
}
110110
}
111111

112+
@Test
113+
public void readHeaderFrameAndContinuationFrameExceedMax() throws Http2Exception {
114+
frameReader = new DefaultHttp2FrameReader(new DefaultHttp2HeadersDecoder(true), 2);
115+
final int streamId = 1;
116+
117+
final ByteBuf input = Unpooled.buffer();
118+
try {
119+
Http2Headers headers = new DefaultHttp2Headers()
120+
.authority("foo")
121+
.method("get")
122+
.path("/")
123+
.scheme("https");
124+
writeHeaderFrame(input, streamId, headers,
125+
new Http2Flags().endOfHeaders(false).endOfStream(true));
126+
writeContinuationFrame(input, streamId, new DefaultHttp2Headers().add("foo", "bar"),
127+
new Http2Flags().endOfHeaders(false));
128+
writeContinuationFrame(input, streamId, new DefaultHttp2Headers().add("foo2", "bar2"),
129+
new Http2Flags().endOfHeaders(false));
130+
131+
Http2Exception ex = assertThrows(Http2Exception.class, new Executable() {
132+
@Override
133+
public void execute() throws Throwable {
134+
frameReader.readFrame(ctx, input, listener);
135+
}
136+
});
137+
assertEquals(Http2Error.ENHANCE_YOUR_CALM, ex.error());
138+
} finally {
139+
input.release();
140+
}
141+
}
142+
143+
@Test
144+
public void readHeaderFrameAndContinuationFrameDontExceedMax() throws Http2Exception {
145+
frameReader = new DefaultHttp2FrameReader(new DefaultHttp2HeadersDecoder(true), 2);
146+
final int streamId = 1;
147+
148+
final ByteBuf input = Unpooled.buffer();
149+
try {
150+
Http2Headers headers = new DefaultHttp2Headers()
151+
.authority("foo")
152+
.method("get")
153+
.path("/")
154+
.scheme("https");
155+
writeHeaderFrame(input, streamId, headers,
156+
new Http2Flags().endOfHeaders(false).endOfStream(true));
157+
writeContinuationFrame(input, streamId, new DefaultHttp2Headers().add("foo", "bar"),
158+
new Http2Flags().endOfHeaders(false));
159+
frameReader.readFrame(ctx, input, listener);
160+
} finally {
161+
input.release();
162+
}
163+
}
164+
112165
@Test
113166
public void readUnknownFrame() throws Http2Exception {
114167
ByteBuf input = Unpooled.buffer();

0 commit comments

Comments
 (0)