Skip to content

Commit

Permalink
Merge pull request from GHSA-pgfx-g6rc-8cjv
Browse files Browse the repository at this point in the history
Motivation:

We don't currently support ALTSVC and ORIGIN frames. At the moment when
receiving or attempting to write these frames we trap. Trapping when
receiving an unsupported frame can lead to DoS attacks. Moreover these
frames are non critical so can safely be ignored.

Modifications:

- Forward inbound and outbound ALTSVC and ORIGIN frames to the
  connection state machine
- The connection state machine now ignores inbound frames of this type
- The connection state machine traps on outbound frames of this type (as
  per the current behaviour)
- Document this behaviour on `HTTP2Frame.FramePayload`
- Tests and fuzz testing failure case

Result:

We don't trap when receiving unsupported frames.
  • Loading branch information
glbrntt committed Feb 11, 2022
1 parent 56c60a2 commit 000ca94
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 7 deletions.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -1441,6 +1441,45 @@ extension HTTP2ConnectionStateMachine {
preconditionFailure("Must not be left in modifying state")
}
}

/// Called when an ALTSVC frame has been received.
///
/// At present the frame is unconditionally ignored.
mutating func receiveAlternativeService(origin: String?, field: ByteBuffer?) -> StateMachineResultWithEffect {
// We don't support ALTSVC frames right now so we just ignore them.
//
// From RFC 7838 § 4:
// > The ALTSVC frame is a non-critical extension to HTTP/2. Endpoints
// > that do not support this frame will ignore it (as per the
// > extensibility rules defined in Section 4.1 of RFC7540).
return .init(result: .ignoreFrame, effect: .none)
}

/// Called when an ALTSVC frame is sent.
///
/// At present the frame is not handled, calling this function will trap.
mutating func sendAlternativeService(origin: String?, field: ByteBuffer?) -> Never {
fatalError("Currently ALTSVC frames are unhandled.")
}

/// Called when an ORIGIN frame has been received.
///
/// At present the frame is unconditionally ignored.
mutating func receiveOrigin(origins: [String]) -> StateMachineResultWithEffect {
// We don't support ORIGIN frames right now so we just ignore them.
//
// From RFC 8336 § 2.1:
// > The ORIGIN frame is a non-critical extension to HTTP/2. Endpoints
// > that do not support this frame can safely ignore it upon receipt.
return .init(result: .ignoreFrame, effect: .none)
}

/// Called when an ORIGIN frame is sent.
///
/// At present the frame is not handled, calling this function will trap.
mutating func sendOrigin(origins: [String]) -> Never {
fatalError("Currently ORIGIN frames are unhandled.")
}
}

// Mark:- Private helper methods
Expand Down
16 changes: 10 additions & 6 deletions Sources/NIOHTTP2/HTTP2ChannelHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,10 @@ extension NIOHTTP2Handler {
var result: StateMachineResultWithEffect

switch frame.payload {
case .alternativeService, .origin:
// TODO(cory): Implement
fatalError("Currently some frames are unhandled.")
case .alternativeService(let origin, let field):
result = self.stateMachine.receiveAlternativeService(origin: origin, field: field)
case .origin(let origins):
result = self.stateMachine.receiveOrigin(origins: origins)
case .data(let dataBody):
result = self.stateMachine.receiveData(streamID: frame.streamID, contentLength: dataBody.data.readableBytes, flowControlledBytes: flowControlledLength, isEndStreamSet: dataBody.endStream)
case .goAway(let lastStreamID, _, _):
Expand Down Expand Up @@ -422,9 +423,12 @@ extension NIOHTTP2Handler {
let result: StateMachineResultWithEffect

switch frame.payload {
case .alternativeService, .origin:
// TODO(cory): Implement
fatalError("Currently some frames are unhandled.")
case .alternativeService(let origin, let field):
// Returns 'Never'; alt service frames are not currently handled.
self.stateMachine.sendAlternativeService(origin: origin, field: field)
case .origin(let origins):
// Returns 'Never'; origin frames are not currently handled.
self.stateMachine.sendOrigin(origins: origins)
case .data(let data):
// TODO(cory): Correctly account for padding data.
result = self.stateMachine.sendData(streamID: frame.streamID, contentLength: data.data.readableBytes, flowControlledBytes: data.data.readableBytes, isEndStreamSet: data.endStream)
Expand Down
6 changes: 6 additions & 0 deletions Sources/NIOHTTP2/HTTP2Frame.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,19 @@ public struct HTTP2Frame {
/// the locations at which they may be addressed.
///
/// See [RFC 7838 § 4](https://tools.ietf.org/html/rfc7838#section-4).
///
/// - Important: ALTSVC frames are not currently supported. Any received ALTSVC frames will
/// be ignored. Attempting to send an ALTSVC frame will result in a fatal error.
indirect case alternativeService(origin: String?, field: ByteBuffer?)

/// An ORIGIN frame. This allows servers which allow access to multiple origins
/// via the same socket connection to identify which origins may be accessed in
/// this manner.
///
/// See [RFC 8336 § 2](https://tools.ietf.org/html/rfc8336#section-2).
///
/// - Important: ORIGIN frames are not currently supported. Any received ORIGIN frames will
/// be ignored. Attempting to send an ORIGIN frame will result in a fatal error.
case origin([String])

/// The payload of a DATA frame.
Expand Down
2 changes: 2 additions & 0 deletions Tests/NIOHTTP2Tests/ConnectionStateMachineTests+XCTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ extension ConnectionStateMachineTests {
("testNoPolicingInvalidContentLengthForRequestsWithEndStreamWhenValidationDisabled", testNoPolicingInvalidContentLengthForRequestsWithEndStreamWhenValidationDisabled),
("testNoPolicingInvalidContentLengthForResponsesWithEndStreamWhenValidationDisabled", testNoPolicingInvalidContentLengthForResponsesWithEndStreamWhenValidationDisabled),
("testWeTolerateOneStreamBeingResetTwice", testWeTolerateOneStreamBeingResetTwice),
("testReceivedAltServiceFramesAreIgnored", testReceivedAltServiceFramesAreIgnored),
("testReceivedOriginFramesAreIgnored", testReceivedOriginFramesAreIgnored),
]
}
}
Expand Down
12 changes: 12 additions & 0 deletions Tests/NIOHTTP2Tests/ConnectionStateMachineTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2974,6 +2974,18 @@ class ConnectionStateMachineTests: XCTestCase {
assertSucceeds(self.server.receiveRstStream(streamID: streamOne, reason: .cancel))
assertIgnored(self.server.receiveRstStream(streamID: streamOne, reason: .streamClosed))
}

func testReceivedAltServiceFramesAreIgnored() {
self.exchangePreamble()
assertIgnored(self.client.receiveAlternativeService(origin: "over-there", field: nil))
assertIgnored(self.server.receiveAlternativeService(origin: "over-there", field: nil))
}

func testReceivedOriginFramesAreIgnored() {
self.exchangePreamble()
assertIgnored(self.client.receiveOrigin(origins: ["one", "two"]))
assertIgnored(self.server.receiveOrigin(origins: ["one", "two"]))
}
}


Expand Down
2 changes: 1 addition & 1 deletion Tests/NIOHTTP2Tests/HTTP2FrameParserTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1806,7 +1806,7 @@ class HTTP2FrameParserTests: XCTestCase {

let frameBytes: [UInt8] = [
0x00, 0x00, 0x2a, // 3-byte payload length (42 bytes)
0x0c, // 1-byte frame type (ALTSVC)
0x0c, // 1-byte frame type (ORIGIN)
0x00, // 1-byte flags (none)
0x00, 0x00, 0x00, 0x00, // 4-byte stream identifier,
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ extension SimpleClientServerFramePayloadStreamTests {
("testStreamCreationOrder", testStreamCreationOrder),
("testStreamClosedInvalidRequestHeaders", testStreamClosedInvalidRequestHeaders),
("testHTTP2HandlerDoesNotFlushExcessively", testHTTP2HandlerDoesNotFlushExcessively),
("testHTTPHandlerIgnoresInboundAltServiceFrames", testHTTPHandlerIgnoresInboundAltServiceFrames),
("testHTTPHandlerIgnoresInboundOriginFrames", testHTTPHandlerIgnoresInboundOriginFrames),
]
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1982,4 +1982,61 @@ class SimpleClientServerFramePayloadStreamTests: XCTestCase {
self.clientChannel.pipeline.fireChannelReadComplete()
XCTAssertEqual(counter.flushCount, 0)
}

func testHTTPHandlerIgnoresInboundAltServiceFrames() throws {
try self.basicHTTP2Connection()

let h2ClientHandler = try self.clientChannel.pipeline.syncOperations.handler(type: NIOHTTP2Handler.self)
let h2FrameRecorder = FrameRecorderHandler()
try self.clientChannel.pipeline.syncOperations.addHandler(h2FrameRecorder, position: .after(h2ClientHandler))

let altServiceFrameBytes: [UInt8] = [
0x00, 0x00, 0x15, // 3-byte payload length (21 bytes)
0x0a, // 1-byte frame type (ALTSVC)
0x00, // 1-byte flags (none)
0x00, 0x00, 0x00, 0x00, // 4-byte stream identifier
0x00, 0x09, // 2-byte origin size ("apple.com"; 9 bytes)
]

var buffer = self.clientChannel.allocator.buffer(bytes: altServiceFrameBytes)
buffer.writeString("apple.com")
buffer.writeString("h2=\":8000\"")
XCTAssertEqual(buffer.readableBytes, 30) // 9 (header) + 21 (origin, field)
XCTAssertNoThrow(try self.clientChannel.writeInbound(buffer))

// Frame should be dropped.
XCTAssert(h2FrameRecorder.receivedFrames.isEmpty)
}

func testHTTPHandlerIgnoresInboundOriginFrames() throws {
try self.basicHTTP2Connection()

let h2ClientHandler = try self.clientChannel.pipeline.syncOperations.handler(type: NIOHTTP2Handler.self)
let h2FrameRecorder = FrameRecorderHandler()
try self.clientChannel.pipeline.syncOperations.addHandler(h2FrameRecorder, position: .after(h2ClientHandler))

let originFrameBytes: [UInt8] = [
0x00, 0x00, 0x2a, // 3-byte payload length (42 bytes)
0x0c, // 1-byte frame type (ORIGIN)
0x00, // 1-byte flags (none)
0x00, 0x00, 0x00, 0x00 // 4-byte stream identifier,
]

var buffer = self.clientChannel.allocator.buffer(bytes: originFrameBytes)
var originBytesWritten = 0

for origin in ["apple.com", "www.apple.com", "www2.apple.com"] {
originBytesWritten += try buffer.writeLengthPrefixed(as: UInt16.self) {
$0.writeString(origin)
}
}

XCTAssertEqual(originBytesWritten, 42)
XCTAssertEqual(buffer.readableBytes, 51)

XCTAssertNoThrow(try self.clientChannel.writeInbound(buffer))

// Frame should be dropped.
XCTAssert(h2FrameRecorder.receivedFrames.isEmpty)
}
}

0 comments on commit 000ca94

Please sign in to comment.