Skip to content

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.

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

/// 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

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.

}

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

/// 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

/// 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.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

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

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
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


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
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

}

// 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

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

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?

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
@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.

4 participants