-
Notifications
You must be signed in to change notification settings - Fork 87
Reject long sequences of CONTINUATION frames #443
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
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.
FranzBusch
left a comment
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
| /// 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
| 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.
| } | ||
|
|
||
| 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
| /// 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
| /// 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.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
| 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
| 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
| 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
|
|
||
| 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
| 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 |
| } | ||
|
|
||
| // 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.
|
|
||
| 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 |
FranzBusch
left a comment
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
| 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?
FranzBusch
left a comment
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.