-
Notifications
You must be signed in to change notification settings - Fork 84
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
Reject long sequences of CONTINUATION frames #443
Reject long sequences of CONTINUATION frames #443
Conversation
Motivation: Long sequences of CONTINUATION frames can be used to mount attacks by attempting to get a remote peer to consume large amounts of memory. Modifications: - Add a limit to the number of sequential CONTINUATION frames that can be received. This limit is configurable by users at the NIOHTTP2Handler level and has a default value of 5. When this limit is exceeded, the recipient responds with a GOAWAY frame and an "Enhance Your Calm" error of a newly created type `ExcessiveContinuationFrames`. Result: Long sequences of CONTINUATION frames are now rejected by the recipient.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great. I left a comment w.r.t. the changes in the public API
@@ -228,19 +232,22 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler { | |||
/// - maximumBufferedControlFrames: Controls the maximum buffer size of buffered outbound control frames. If we are unable to send control frames as | |||
/// fast as we produce them we risk building up an unbounded buffer and exhausting our memory. To protect against this DoS vector, we put an | |||
/// upper limit on the depth of this queue. Defaults to 10,000. | |||
/// - maximumSequentialContinuationFrames: The maximum number of sequential CONTINUATION frames. | |||
public convenience init(mode: ParserMode, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a breaking API change. We can't add a new parameter to a public method even if we default it since somebody could reference the init/method. That's the reason we introduced ConnectionConfiguration
which is extensible. So let's remove any changes from the public inits here and just change the ConnectionConfiguration
struct
@@ -133,6 +133,8 @@ class ConnectionStateMachineTests: XCTestCase { | |||
var clientEncoder: HTTP2FrameEncoder! | |||
var clientDecoder: HTTP2FrameDecoder! | |||
|
|||
var maximumSequentialContinuationFrames: Int = 5 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be a let
, changing it after setUp
has been called (which happens before the test is run) will have no effect.
@@ -785,6 +835,61 @@ class HTTP2FrameParserTests: XCTestCase { | |||
try assertReadsFrame(from: buf, matching: expectedFrame) | |||
} | |||
|
|||
func testMaximumSequentialContinuationFrames() throws { | |||
let CONTINUATION: [UInt8] = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no need to use shouty-case here, let continuationBytes
is fine
// CONTINUATION frame with the END_HEADERS flag not set | ||
0x00, 0x00, 0x00, // 3-byte payload length (0 bytes) | ||
0x09, // 1-byte frame type (CONTINUATION) | ||
0x00, // 1-byte flags (END_HEADERS) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The end headers flag isn't set here but the comment says it is
@@ -31,9 +31,11 @@ struct HTTP2FrameDecoder { | |||
/// The state for a parser that is waiting for the client magic. | |||
private struct ClientMagicState { | |||
private var pendingBytes: ByteBuffer? | |||
private var maximumSequentialContinuationFrames: Int |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this value should ever change, it should be a let
@@ -58,14 +63,16 @@ struct HTTP2FrameDecoder { | |||
/// The state for a parser that is currently accumulating the bytes of a frame header. | |||
private struct AccumulatingFrameHeaderParserState { | |||
private(set) var unusedBytes: ByteBuffer | |||
private(set) var maximumSequentialContinuationFrames: Int |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, can this ever change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it can't.
init( | ||
fromIdle state: AccumulatingFrameHeaderParserState, | ||
header: FrameHeader, | ||
maximumSequentialContinuationFrames: Int |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, we can just copy it from state
rather than pasing it in separately
self.header = header | ||
self.payload = state.unusedBytes | ||
self.expectedPadding = nil | ||
self.remainingByteCount = remainingBytes | ||
self.flowControlledLength = header.length | ||
self.maximumSequentialContinuationFrames = maximumSequentialContinuationFrames |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here re: copying from state
precondition(expectedPadding > 0) | ||
|
||
self.header = state.header | ||
self.excessBytes = excessBytes | ||
self.expectedPadding = expectedPadding | ||
self.maximumSequentialContinuationFrames = maximumSequentialContinuationFrames |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here, let's just copy from state
precondition(expectedPadding > 0) | ||
|
||
self.header = state.header | ||
self.excessBytes = excessBytes | ||
self.expectedPadding = expectedPadding | ||
self.maximumSequentialContinuationFrames = maximumSequentialContinuationFrames |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here, let's just copy from state
|
||
init(fromAccumulatingHeaderBlockFragments acc: AccumulatingHeaderBlockFragmentsParserState, | ||
continuationHeader: FrameHeader) { | ||
private var sequentialContinuationFramesSize: Int |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: prefer count
to size
when counting things
@@ -104,6 +104,9 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler { | |||
/// The delegate for (de)multiplexing inbound streams. | |||
private var inboundStreamMultiplexerState: InboundStreamMultiplexerState | |||
|
|||
/// The maximum number of sequential CONTINUATION frames. | |||
private var maximumSequentialContinuationFrames: Int |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be a constant
@@ -685,6 +775,7 @@ struct HTTP2FrameDecoder { | |||
|
|||
internal var headerDecoder: HPACKDecoder | |||
private var state: ParserState | |||
private var maximumSequentialContinuationFrames: Int |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be a let
. Also we don't need to pass maximumSequentialContinuationFrames
through all of the states, I think we only need to pass it to the process
func of the appropriate state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also we don't need to pass maximumSequentialContinuationFrames through all of the states, I think we only need to pass it to the process func of the appropriate state.
Yes. I completely missed that. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should still be a let
- Refactor implementation - Revert unnecessary diffs
@@ -1109,6 +1129,7 @@ extension NIOHTTP2Handler { | |||
public var contentLengthValidation: ValidationState = .enabled | |||
public var maximumSequentialEmptyDataFrames: Int = 1 | |||
public var maximumBufferedControlFrames: Int = 10000 | |||
public let maximumSequentialContinuationFrames: Int = 5 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be var
as its a config that users can adjust before passing in
public let maximumSequentialContinuationFrames: Int = 5 | |
public var maximumSequentialContinuationFrames: Int = 5 |
@@ -601,6 +619,11 @@ struct HTTP2FrameDecoder { | |||
throw NIOHTTP2Errors.excessivelyLargeHeaderBlock() | |||
} | |||
|
|||
// The sequence of CONTINUATION frames received is not longer than the configured limit | |||
guard self.sequentialContinuationFramesCount < maximumSequentialContinuationFrames else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be <=
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it shouldn't be. But I have made some adjustments to make the condition clearer.
try self.processNextState( | ||
maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need other pass this in here: maximumSequentialContinuationFrames
is already stored on self
so we can just read it from self
in processNextState
- Change `public let maximumSequentialContinuationFrames` to `public var`. - Refactor implementation.
@@ -685,6 +775,7 @@ struct HTTP2FrameDecoder { | |||
|
|||
internal var headerDecoder: HPACKDecoder | |||
private var state: ParserState | |||
private var maximumSequentialContinuationFrames: Int |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should still be a let
switch ( | ||
try self.processNextState() | ||
) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
switch ( | |
try self.processNextState() | |
) { | |
switch (try self.processNextState()) { |
0x00, // payload | ||
|
||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
0x00, // payload | |
] | |
0x00, // payload | |
] |
) | ||
|
||
let headersFrame: [UInt8] = [ | ||
0x00, 0x00, 0x01, // 3-byte payload length (1 byte) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we have a 1-byte payload?
var frameDecoder = HTTP2FrameDecoder(allocator: ByteBufferAllocator(), expectClientMagic: mode == .client) | ||
func drainConnectionSetupWrites( | ||
mode: NIOHTTP2Handler.ParserMode = .server, | ||
maximumSequentialContinuationFrames: Int |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we just default this to 5? Doing that means we can avoid all the other changes in this file.
@swift-server-bot add to allowlist |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM just one nit
@@ -212,6 +215,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler { | |||
contentLengthValidation: contentLengthValidation, | |||
maximumSequentialEmptyDataFrames: 1, | |||
maximumBufferedControlFrames: 10000, | |||
maximumSequentialContinuationFrames: 5, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have one more nit here. Instead of respelling this default of 5 in multiple places can we pull it out into one static let that we reference?
… static let`
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome! LGTM
Motivation:
Long sequences of CONTINUATION frames can be used to mount attacks by attempting to get a remote peer to consume large amounts of memory.
Modifications:
ExcessiveContinuationFrames
.Result:
Long sequences of CONTINUATION frames are now rejected by the recipient.