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

Reject long sequences of CONTINUATION frames #443

Merged

Conversation

clintonpi
Copy link
Contributor

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.

Verified

This commit was signed with the committer’s verified signature.
vbudhram Vijay Budhram
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.
Copy link
Member

@FranzBusch FranzBusch left a 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,
Copy link
Member

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
Copy link
Contributor

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] = [
Copy link
Contributor

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)
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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?

Copy link
Contributor Author

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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

@clintonpi clintonpi requested review from FranzBusch and glbrntt June 25, 2024 09:42
@@ -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
Copy link
Contributor

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
Copy link
Contributor

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.

Copy link
Contributor Author

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!

Copy link
Contributor

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

@Lukasa Lukasa added the 🔨 semver/patch No public API change. label Jun 26, 2024
- Refactor implementation
- Revert unnecessary diffs
@clintonpi clintonpi requested a review from glbrntt June 26, 2024 15:32
@@ -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
Copy link
Contributor

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

Suggested change
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be <=?

Copy link
Contributor Author

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.

Comment on lines 803 to 805
try self.processNextState(
maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
)
Copy link
Contributor

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.
@clintonpi clintonpi requested a review from glbrntt June 28, 2024 08:59
@@ -685,6 +775,7 @@ struct HTTP2FrameDecoder {

internal var headerDecoder: HPACKDecoder
private var state: ParserState
private var maximumSequentialContinuationFrames: Int
Copy link
Contributor

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

Comment on lines 799 to 801
switch (
try self.processNextState()
) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
switch (
try self.processNextState()
) {
switch (try self.processNextState()) {

Comment on lines 1810 to 1812
0x00, // payload

]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
0x00, // payload
]
0x00, // payload
]

)

let headersFrame: [UInt8] = [
0x00, 0x00, 0x01, // 3-byte payload length (1 byte)
Copy link
Contributor

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
Copy link
Contributor

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.

@clintonpi clintonpi requested a review from glbrntt June 28, 2024 16:02
@glbrntt
Copy link
Contributor

glbrntt commented Jun 28, 2024

@swift-server-bot add to allowlist

Copy link
Member

@FranzBusch FranzBusch left a 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,
Copy link
Member

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`
Copy link
Member

@FranzBusch FranzBusch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome! LGTM

@FranzBusch FranzBusch merged commit d629013 into apple:main Jul 4, 2024
6 of 7 checks passed
@clintonpi clintonpi deleted the reject-long-sequences-of-continuation-frames branch July 4, 2024 13:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🔨 semver/patch No public API change.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants