Repository: apple/swift-nio-http2
Pull request #443 (2024-06-24 09:34:56)

Reject long sequences of CONTINUATION frames
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.

glbrntt: @swift-server-bot add to allowlist

Patch:
diff --git a/Sources/NIOHTTP2/HTTP2ChannelHandler.swift b/Sources/NIOHTTP2/HTTP2ChannelHandler.swift
index fab1ae3..6908e45 100644
--- a/Sources/NIOHTTP2/HTTP2ChannelHandler.swift
+++ b/Sources/NIOHTTP2/HTTP2ChannelHandler.swift
@@ -39,6 +39,9 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
     /// The magic string sent by clients at the start of a HTTP/2 connection.
     private static let clientMagic: StaticString = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
 
+    /// The default value for the maximum number of sequential CONTINUATION frames.
+    private static let defaultMaximumSequentialContinuationFrames: Int = 5
+
     /// The event loop on which this handler will do work.
     @usableFromInline internal let _eventLoop: EventLoop?
 
@@ -104,6 +107,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 let maximumSequentialContinuationFrames: Int
+
     @usableFromInline
     internal var inboundStreamMultiplexer: InboundStreamMultiplexer? {
         return self.inboundStreamMultiplexerState.multiplexer
@@ -212,6 +218,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
                   contentLengthValidation: contentLengthValidation,
                   maximumSequentialEmptyDataFrames: 1,
                   maximumBufferedControlFrames: 10000,
+                  maximumSequentialContinuationFrames: NIOHTTP2Handler.defaultMaximumSequentialContinuationFrames,
                   maximumResetFrameCount: 200,
                   resetFrameCounterWindow: .seconds(30))
     }
@@ -241,6 +248,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
                   contentLengthValidation: contentLengthValidation,
                   maximumSequentialEmptyDataFrames: maximumSequentialEmptyDataFrames,
                   maximumBufferedControlFrames: maximumBufferedControlFrames,
+                  maximumSequentialContinuationFrames: NIOHTTP2Handler.defaultMaximumSequentialContinuationFrames,
                   maximumResetFrameCount: 200,
                   resetFrameCounterWindow: .seconds(30))
 
@@ -262,6 +270,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
                   contentLengthValidation: connectionConfiguration.contentLengthValidation,
                   maximumSequentialEmptyDataFrames: connectionConfiguration.maximumSequentialEmptyDataFrames,
                   maximumBufferedControlFrames: connectionConfiguration.maximumBufferedControlFrames,
+                  maximumSequentialContinuationFrames: connectionConfiguration.maximumSequentialContinuationFrames,
                   maximumResetFrameCount: streamConfiguration.streamResetFrameRateLimit.maximumCount,
                   resetFrameCounterWindow: streamConfiguration.streamResetFrameRateLimit.windowLength)
     }
@@ -273,6 +282,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
                  contentLengthValidation: ValidationState,
                  maximumSequentialEmptyDataFrames: Int,
                  maximumBufferedControlFrames: Int,
+                 maximumSequentialContinuationFrames: Int,
                  maximumResetFrameCount: Int,
                  resetFrameCounterWindow: TimeAmount) {
         self._eventLoop = eventLoop
@@ -283,6 +293,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
         self.denialOfServiceValidator = DOSHeuristics(maximumSequentialEmptyDataFrames: maximumSequentialEmptyDataFrames, maximumResetFrameCount: maximumResetFrameCount, resetFrameCounterWindow: resetFrameCounterWindow)
         self.tolerateImpossibleStateTransitionsInDebugMode = false
         self.inboundStreamMultiplexerState = .uninitializedLegacy
+        self.maximumSequentialContinuationFrames = maximumSequentialContinuationFrames
     }
 
     /// Constructs a ``NIOHTTP2Handler``.
@@ -297,6 +308,7 @@ 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.
     ///   - tolerateImpossibleStateTransitionsInDebugMode: Whether impossible state transitions should be tolerated
     ///         in debug mode.
     ///   - maximumResetFrameCount: Controls the maximum permitted reset frames within a given time window. Too many may exhaust CPU resources. To protect
@@ -309,6 +321,7 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
                   contentLengthValidation: ValidationState = .enabled,
                   maximumSequentialEmptyDataFrames: Int = 1,
                   maximumBufferedControlFrames: Int = 10000,
+                  maximumSequentialContinuationFrames: Int = NIOHTTP2Handler.defaultMaximumSequentialContinuationFrames,
                   tolerateImpossibleStateTransitionsInDebugMode: Bool = false,
                   maximumResetFrameCount: Int = 200,
                   resetFrameCounterWindow: TimeAmount = .seconds(30)) {
@@ -320,10 +333,15 @@ public final class NIOHTTP2Handler: ChannelDuplexHandler {
         self.denialOfServiceValidator = DOSHeuristics(maximumSequentialEmptyDataFrames: maximumSequentialEmptyDataFrames, maximumResetFrameCount: maximumResetFrameCount, resetFrameCounterWindow: resetFrameCounterWindow)
         self.tolerateImpossibleStateTransitionsInDebugMode = tolerateImpossibleStateTransitionsInDebugMode
         self.inboundStreamMultiplexerState = .uninitializedLegacy
+        self.maximumSequentialContinuationFrames = maximumSequentialContinuationFrames
     }
 
     public func handlerAdded(context: ChannelHandlerContext) {
-        self.frameDecoder = HTTP2FrameDecoder(allocator: context.channel.allocator, expectClientMagic: self.mode == .server)
+        self.frameDecoder = HTTP2FrameDecoder(
+            allocator: context.channel.allocator,
+            expectClientMagic: self.mode == .server,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
         self.frameEncoder = HTTP2FrameEncoder(allocator: context.channel.allocator)
         self.writeBuffer = context.channel.allocator.buffer(capacity: 128)
         self.inboundStreamMultiplexerState.initialize(context: context, http2Handler: self, mode: self.mode)
@@ -495,6 +513,9 @@ extension NIOHTTP2Handler {
         } catch is NIOHTTP2Errors.ExcessivelyLargeHeaderBlock {
             self.inboundConnectionErrorTriggered(context: context, underlyingError: NIOHTTP2Errors.excessivelyLargeHeaderBlock(), reason: .protocolError)
             return nil
+        } catch is NIOHTTP2Errors.ExcessiveContinuationFrames {
+            self.inboundConnectionErrorTriggered(context: context, underlyingError: NIOHTTP2Errors.excessiveContinuationFrames(), reason: .enhanceYourCalm)
+            return nil
         } catch {
             self.inboundConnectionErrorTriggered(context: context, underlyingError: error, reason: .internalError)
             return nil
@@ -1069,6 +1090,7 @@ extension NIOHTTP2Handler {
             contentLengthValidation: connectionConfiguration.contentLengthValidation,
             maximumSequentialEmptyDataFrames: connectionConfiguration.maximumSequentialEmptyDataFrames,
             maximumBufferedControlFrames: connectionConfiguration.maximumBufferedControlFrames,
+            maximumSequentialContinuationFrames: connectionConfiguration.maximumSequentialContinuationFrames,
             maximumResetFrameCount: streamConfiguration.streamResetFrameRateLimit.maximumCount,
             resetFrameCounterWindow: streamConfiguration.streamResetFrameRateLimit.windowLength
         )
@@ -1093,6 +1115,7 @@ extension NIOHTTP2Handler {
             contentLengthValidation: connectionConfiguration.contentLengthValidation,
             maximumSequentialEmptyDataFrames: connectionConfiguration.maximumSequentialEmptyDataFrames,
             maximumBufferedControlFrames: connectionConfiguration.maximumBufferedControlFrames,
+            maximumSequentialContinuationFrames: connectionConfiguration.maximumSequentialContinuationFrames,
             maximumResetFrameCount: streamConfiguration.streamResetFrameRateLimit.maximumCount,
             resetFrameCounterWindow: streamConfiguration.streamResetFrameRateLimit.windowLength
         )
@@ -1109,6 +1132,7 @@ extension NIOHTTP2Handler {
         public var contentLengthValidation: ValidationState = .enabled
         public var maximumSequentialEmptyDataFrames: Int = 1
         public var maximumBufferedControlFrames: Int = 10000
+        public var maximumSequentialContinuationFrames: Int = NIOHTTP2Handler.defaultMaximumSequentialContinuationFrames
         public init() {}
     }
 
diff --git a/Sources/NIOHTTP2/HTTP2Error.swift b/Sources/NIOHTTP2/HTTP2Error.swift
index 5bb0ab4..e41d096 100644
--- a/Sources/NIOHTTP2/HTTP2Error.swift
+++ b/Sources/NIOHTTP2/HTTP2Error.swift
@@ -298,6 +298,11 @@ public enum NIOHTTP2Errors {
         return ExcessiveRSTFrames(file: file, line: line)
     }
 
+    /// Creates an ``ExcessiveContinuationFrames`` error with appropriate source context.
+    public static func excessiveContinuationFrames(file: String = #fileID, line: UInt = #line) -> ExcessiveContinuationFrames {
+        return ExcessiveContinuationFrames(file: file, line: line)
+    }
+
     /// Creates a ``StreamError`` error with appropriate source context.
     ///
     /// - Parameters:
@@ -1754,6 +1759,26 @@ public enum NIOHTTP2Errors {
             return true
         }
     }
+
+    /// A remote peer has sent a sequence of `CONTINUATION` frames longer than the configured limit.
+    public struct ExcessiveContinuationFrames: NIOHTTP2Error {
+        private let file: String
+        private let line: UInt
+
+        /// The location where the error was thrown.
+        public var location: String {
+            return _location(file: self.file, line: self.line)
+        }
+
+        fileprivate init(file: String, line: UInt) {
+            self.file = file
+            self.line = line
+        }
+
+        public static func ==(lhs: Self, rhs: Self) -> Bool {
+            return true
+        }
+    }
 }
 
 
diff --git a/Sources/NIOHTTP2/HTTP2FrameParser.swift b/Sources/NIOHTTP2/HTTP2FrameParser.swift
index 3ba6066..90f52f2 100644
--- a/Sources/NIOHTTP2/HTTP2FrameParser.swift
+++ b/Sources/NIOHTTP2/HTTP2FrameParser.swift
@@ -277,7 +277,8 @@ struct HTTP2FrameDecoder {
                         header: syntheticHeader,
                         initialPayload: payloadBytes,
                         incomingPayload: self.accumulatedBytes,
-                        originalPaddingBytes: self.expectedPadding
+                        originalPaddingBytes: self.expectedPadding,
+                        continuationSequenceCount: 1
                     )
                 )
             }
@@ -504,6 +505,7 @@ struct HTTP2FrameDecoder {
         private var currentFrameBytes: ByteBuffer
         private var continuationPayload: ByteBuffer
         private var originalPaddingBytes: Int?
+        private var continuationSequenceCount: Int
 
         init(fromAccumulatingHeaderBlockFragments acc: AccumulatingHeaderBlockFragmentsParserState,
              continuationHeader: FrameHeader) {
@@ -515,6 +517,7 @@ struct HTTP2FrameDecoder {
             self.currentFrameBytes = acc.accumulatedPayload
             self.continuationPayload = acc.incomingPayload
             self.originalPaddingBytes = acc.originalPaddingBytes
+            self.continuationSequenceCount = acc.continuationSequenceCount
         }
 
         /// The result of successful processing: we either produce a frame and move to the new accumulating state,
@@ -556,7 +559,8 @@ struct HTTP2FrameDecoder {
                         header: header,
                         initialPayload: payload,
                         incomingPayload: self.continuationPayload,
-                        originalPaddingBytes: self.originalPaddingBytes
+                        originalPaddingBytes: self.originalPaddingBytes,
+                        continuationSequenceCount: self.continuationSequenceCount + 1
                     )
                 )
             }
@@ -582,16 +586,27 @@ struct HTTP2FrameDecoder {
         private(set) var accumulatedPayload: ByteBuffer
         private(set) var incomingPayload: ByteBuffer
         private(set) var originalPaddingBytes: Int?
-
-        init(header: FrameHeader, initialPayload: ByteBuffer, incomingPayload: ByteBuffer, originalPaddingBytes: Int?) {
+        private(set) var continuationSequenceCount: Int
+
+        init(
+            header: FrameHeader,
+            initialPayload: ByteBuffer,
+            incomingPayload: ByteBuffer,
+            originalPaddingBytes: Int?,
+            continuationSequenceCount: Int
+        ) {
             precondition(header.beginsContinuationSequence)
             self.header = header
             self.accumulatedPayload = initialPayload
             self.incomingPayload = incomingPayload
             self.originalPaddingBytes = originalPaddingBytes
+            self.continuationSequenceCount = continuationSequenceCount
         }
 
-        mutating func process(maxHeaderListSize: Int) throws -> AccumulatingContinuationPayloadParserState? {
+        mutating func process(
+            maxHeaderListSize: Int,
+            maximumSequentialContinuationFrames: Int
+        ) throws -> AccumulatingContinuationPayloadParserState? {
             // we have an entire HEADERS/PUSH_PROMISE frame, but one or more CONTINUATION frames
             // are arriving. Wait for them.
             guard let header = self.incomingPayload.readFrameHeader() else {
@@ -614,6 +629,11 @@ struct HTTP2FrameDecoder {
                 throw NIOHTTP2Errors.excessivelyLargeHeaderBlock()
             }
 
+            // The sequence of CONTINUATION frames received is not longer than the configured limit
+            guard self.continuationSequenceCount <= maximumSequentialContinuationFrames else {
+                throw NIOHTTP2Errors.excessiveContinuationFrames()
+            }
+
             return AccumulatingContinuationPayloadParserState(fromAccumulatingHeaderBlockFragments: self, continuationHeader: header)
         }
 
@@ -698,6 +718,7 @@ struct HTTP2FrameDecoder {
 
     internal var headerDecoder: HPACKDecoder
     private var state: ParserState
+    private let maximumSequentialContinuationFrames: Int
 
     // RFC 7540 § 6.5.2 puts the initial value of SETTINGS_MAX_FRAME_SIZE at 2**14 octets
     internal var maxFrameSize: UInt32 = 1<<14
@@ -708,8 +729,14 @@ struct HTTP2FrameDecoder {
     ///                        and decoding headers.
     /// - parameter expectClientMagic: Whether the parser should expect to receive the bytes of
     ///                                client magic string before frame parsing begins.
-    init(allocator: ByteBufferAllocator, expectClientMagic: Bool) {
+    /// - parameter maximumSequentialContinuationFrames: The maximum number of sequential CONTINUATION frames.
+    init(
+        allocator: ByteBufferAllocator,
+        expectClientMagic: Bool,
+        maximumSequentialContinuationFrames: Int
+    ) {
         self.headerDecoder = HPACKDecoder(allocator: allocator)
+        self.maximumSequentialContinuationFrames = maximumSequentialContinuationFrames
 
         if expectClientMagic {
             self.state = .awaitingClientMagic(ClientMagicState())
@@ -1001,9 +1028,14 @@ struct HTTP2FrameDecoder {
 
         case .accumulatingHeaderBlockFragments(var state):
             let maxHeaderListSize = self.headerDecoder.maxHeaderListSize
+            let maximumSequentialContinuationFrames = self.maximumSequentialContinuationFrames
+
             return try self.avoidingParserCoW { newState in
                 let result = Result<ParseResult, Error> {
-                    guard let processResult = try state.process(maxHeaderListSize: maxHeaderListSize) else {
+                    guard let processResult = try state.process(
+                        maxHeaderListSize: maxHeaderListSize,
+                        maximumSequentialContinuationFrames: maximumSequentialContinuationFrames
+                    ) else {
                         newState = .accumulatingHeaderBlockFragments(state)
                         return .needMoreData
                     }


Test patch:
diff --git a/Tests/NIOHTTP2Tests/ConnectionStateMachineTests.swift b/Tests/NIOHTTP2Tests/ConnectionStateMachineTests.swift
index d6083fc..e4756ff 100644
--- a/Tests/NIOHTTP2Tests/ConnectionStateMachineTests.swift
+++ b/Tests/NIOHTTP2Tests/ConnectionStateMachineTests.swift
@@ -133,6 +133,8 @@ class ConnectionStateMachineTests: XCTestCase {
     var clientEncoder: HTTP2FrameEncoder!
     var clientDecoder: HTTP2FrameDecoder!
 
+    let maximumSequentialContinuationFrames: Int = 5
+
     static let requestHeaders = {
         return HPACKHeaders([(":method", "GET"), (":authority", "localhost"), (":scheme", "https"), (":path", "/"), ("user-agent", "test")])
     }()
@@ -150,9 +152,17 @@ class ConnectionStateMachineTests: XCTestCase {
         self.client = .init(role: .client)
 
         self.serverEncoder = HTTP2FrameEncoder(allocator: ByteBufferAllocator())
-        self.serverDecoder = HTTP2FrameDecoder(allocator: ByteBufferAllocator(), expectClientMagic: true)
+        self.serverDecoder = HTTP2FrameDecoder(
+            allocator: ByteBufferAllocator(),
+            expectClientMagic: true,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
         self.clientEncoder = HTTP2FrameEncoder(allocator: ByteBufferAllocator())
-        self.clientDecoder = HTTP2FrameDecoder(allocator: ByteBufferAllocator(), expectClientMagic: false)
+        self.clientDecoder = HTTP2FrameDecoder(
+            allocator: ByteBufferAllocator(),
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
     }
 
     private func exchangePreamble(client: HTTP2Settings = HTTP2Settings(), server: HTTP2Settings = HTTP2Settings()) {
diff --git a/Tests/NIOHTTP2Tests/HTTP2FrameParserTests.swift b/Tests/NIOHTTP2Tests/HTTP2FrameParserTests.swift
index 98e4b49..c29c749 100644
--- a/Tests/NIOHTTP2Tests/HTTP2FrameParserTests.swift
+++ b/Tests/NIOHTTP2Tests/HTTP2FrameParserTests.swift
@@ -28,7 +28,9 @@ class HTTP2FrameParserTests: XCTestCase {
         (":authority", "www.example.com")
     ])
     let simpleHeadersEncoded: [UInt8] = [0x82, 0x86, 0x84, 0x41, 0x8c, 0xf1, 0xe3, 0xc2, 0xe5, 0xf2, 0x3a, 0x6b, 0xa0, 0xab, 0x90, 0xf4, 0xff]
-    
+
+    let maximumSequentialContinuationFrames = 5
+
     // MARK: - Utilities
     
     private func byteBuffer<C: Collection>(withBytes bytes: C, extraCapacity: Int = 0) -> ByteBuffer where C.Element == UInt8 {
@@ -116,7 +118,11 @@ class HTTP2FrameParserTests: XCTestCase {
                                   file: StaticString = #filePath, line: UInt = #line) throws {
         let totalFrameSize = bytes.readableBytes
         
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
         decoder.append(bytes: bytes)
 
         let (frame, actualLength) = try decoder.nextFrame()!
@@ -220,7 +226,11 @@ class HTTP2FrameParserTests: XCTestCase {
         let firstPaddingBuffer = byteBuffer(withBytes: [UInt8(0)])
         let secondPaddingBuffer = byteBuffer(withBytes: [UInt8(0)])
 
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
 
         decoder.append(bytes: frameBuffer)
         let (frame, actualLength) = try decoder.nextFrame()!
@@ -253,8 +263,12 @@ class HTTP2FrameParserTests: XCTestCase {
         let expectedFrame2 = HTTP2Frame(streamID: HTTP2StreamID(1),
                                         payload: .data(.init(data: .byteBuffer(payloadHalf), endStream: true)))
         
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
-        
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
+
         let slice = buf.readSlice(length: buf.readableBytes - payloadHalf.readableBytes)!
         decoder.append(bytes: slice)
         let frame: HTTP2Frame! = try decoder.nextFrame()?.0
@@ -289,8 +303,12 @@ class HTTP2FrameParserTests: XCTestCase {
         let expectedFrame2 = HTTP2Frame(streamID: HTTP2StreamID(1),
                                         payload: .data(.init(data: .byteBuffer(payloadHalf), endStream: true, paddingBytes: 4)))
         
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
-        
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
+
         let slice = buf.readSlice(length: 9)!
         decoder.append(bytes: slice)
         let noFrame: HTTP2Frame! = try decoder.nextFrame()?.0
@@ -400,7 +418,11 @@ class HTTP2FrameParserTests: XCTestCase {
         let firstPaddingBuffer = byteBuffer(withBytes: [UInt8(0)])
         let secondPaddingBuffer = byteBuffer(withBytes: [UInt8(0)])
 
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
 
         decoder.append(bytes: frameBuffer)
         let (frame, actualLength) = try decoder.nextFrame()!
@@ -483,8 +505,12 @@ class HTTP2FrameParserTests: XCTestCase {
             0x00,                       // payload
         ]
         let badFrameBuf = byteBuffer(withBytes: frameBytes)
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
-        
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
+
         decoder.append(bytes: badFrameBuf)
         XCTAssertThrowsError(try decoder.nextFrame(), "Should throw a protocol error", { err in
             guard let connErr = err as? InternalError, case .codecError(code: .protocolError) = connErr else {
@@ -504,8 +530,12 @@ class HTTP2FrameParserTests: XCTestCase {
                                         // no payload!
         ]
         let buf = self.byteBuffer(withBytes: frameBytes)
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
-        
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
+
         decoder.append(bytes: buf)
         XCTAssertThrowsError(try decoder.nextFrame(), "Should throw a protocol error", { err in
             guard let connErr = err as? InternalError, case .codecError(code: .protocolError) = connErr else {
@@ -524,7 +554,11 @@ class HTTP2FrameParserTests: XCTestCase {
         ]
 
         let buffer = self.allocator.buffer(bytes: frameBytes)
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
         decoder.append(bytes: buffer)
 
         XCTAssertThrowsError(try decoder.nextFrame(), "Should throw a protocol error", { err in
@@ -543,7 +577,11 @@ class HTTP2FrameParserTests: XCTestCase {
             0x00, 0x00, 0x00, 0x01,     // 4-byte stream identifier
         ]
         let badFrameBuf = byteBuffer(withBytes: frameBytes)
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
         XCTAssertEqual(decoder.maxFrameSize, 16384)
 
         decoder.append(bytes: badFrameBuf)
@@ -571,7 +609,11 @@ class HTTP2FrameParserTests: XCTestCase {
         let firstPaddingBuffer = byteBuffer(withBytes: [UInt8(0)])
         let secondPaddingBuffer = byteBuffer(withBytes: [UInt8(0)])
 
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
 
         decoder.append(bytes: frameBuffer)
         var (frame, actualLength) = try decoder.nextFrame()!
@@ -679,8 +721,12 @@ class HTTP2FrameParserTests: XCTestCase {
         buf.writeBytes(self.simpleHeadersEncoded)
         buf.writeBytes([UInt8](repeating: 0, count: 3))
         
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
-        
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
+
         // should fail if the stream is zero
         buf.setInteger(UInt8(0), at: 8)
         decoder.append(bytes: buf)
@@ -717,7 +763,11 @@ class HTTP2FrameParserTests: XCTestCase {
             ]
 
             let buf = self.byteBuffer(withBytes: frameBytes)
-            var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
+            var decoder = HTTP2FrameDecoder(
+                allocator: self.allocator,
+                expectClientMagic: false,
+                maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+            )
 
             decoder.append(bytes: buf)
             XCTAssertThrowsError(try decoder.nextFrame(), "Should throw a protocol error", { err in
@@ -794,7 +844,11 @@ class HTTP2FrameParserTests: XCTestCase {
         ]
 
         let buffer = self.allocator.buffer(bytes: frameBytes)
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
         decoder.append(bytes: buffer)
 
         XCTAssertThrowsError(try decoder.nextFrame(), "Should throw a protocol error", { err in
@@ -815,7 +869,11 @@ class HTTP2FrameParserTests: XCTestCase {
         ]
 
         let buffer = self.allocator.buffer(bytes: frameBytes)
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
         decoder.append(bytes: buffer)
 
         XCTAssertThrowsError(try decoder.nextFrame(), "Should throw a protocol error", { err in
@@ -900,8 +958,12 @@ class HTTP2FrameParserTests: XCTestCase {
         ]
         var buf = byteBuffer(withBytes: frameBytes)
         
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
-        
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
+
         // cannot be on root stream
         buf.setInteger(UInt8(0), at: 8)
         decoder.append(bytes: buf)
@@ -975,8 +1037,12 @@ class HTTP2FrameParserTests: XCTestCase {
         ]
         var buf = byteBuffer(withBytes: frameBytes)
         
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
-        
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
+
         // cannot be on root stream
         buf.setInteger(UInt8(0), at: 8)
         decoder.append(bytes: buf)
@@ -1106,8 +1172,12 @@ class HTTP2FrameParserTests: XCTestCase {
         ]
         var buf = byteBuffer(withBytes: frameBytes)
         
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
-        
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
+
         // MUST be sent on the root stream
         buf.setInteger(UInt8(1), at: 8)
         decoder.append(bytes: buf)
@@ -1183,8 +1253,12 @@ class HTTP2FrameParserTests: XCTestCase {
         ]
         var buf = byteBuffer(withBytes: frameBytes)
         
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
-        
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
+
         // MUST be sent on the root stream
         buf.setInteger(UInt8(1), at: 8)
         decoder.append(bytes: buf)
@@ -1278,8 +1352,12 @@ class HTTP2FrameParserTests: XCTestCase {
         var buf = byteBuffer(withBytes: frameBytes)
         buf.writeBytes(self.simpleHeadersEncoded)
         
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
-        
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
+
         // MUST NOT be sent on the root stream
         buf.setInteger(UInt8(0), at: 8)
         decoder.append(bytes: buf)
@@ -1364,7 +1442,11 @@ class HTTP2FrameParserTests: XCTestCase {
         ]
 
         let buffer = self.allocator.buffer(bytes: frameBytes)
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
         decoder.append(bytes: buffer)
 
         XCTAssertThrowsError(try decoder.nextFrame(), "Should throw a protocol error", { err in
@@ -1386,7 +1468,11 @@ class HTTP2FrameParserTests: XCTestCase {
         ]
 
         let buffer = self.allocator.buffer(bytes: frameBytes)
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
         decoder.append(bytes: buffer)
 
         XCTAssertThrowsError(try decoder.nextFrame(), "Should throw a frame size error", { err in
@@ -1447,8 +1533,12 @@ class HTTP2FrameParserTests: XCTestCase {
             0x04, 0x05, 0x06, 0x07,
         ]
         var buf = byteBuffer(withBytes: frameBytes)
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
-        
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
+
         // MUST NOT be associated with a stream.
         buf.setInteger(UInt8(1), at: 8)
         decoder.append(bytes: buf)
@@ -1558,8 +1648,12 @@ class HTTP2FrameParserTests: XCTestCase {
         ]
         var buf = byteBuffer(withBytes: frameBytes)
         
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
-        
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
+
         // MUST NOT be associated with a stream.
         buf.setInteger(UInt8(1), at: 8)
         decoder.append(bytes: buf)
@@ -1645,8 +1739,12 @@ class HTTP2FrameParserTests: XCTestCase {
         ]
         var buf = byteBuffer(withBytes: frameBytes)
         
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
-        
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
+
         // must have a size of 4 octets
         buf.setInteger(UInt8(5), at: 2)
         buf.writeInteger(UInt8(0))        // append an extra byte so we read it all
@@ -1697,8 +1795,12 @@ class HTTP2FrameParserTests: XCTestCase {
         var buf = byteBuffer(withBytes: frameBytes, extraCapacity: 10)
         buf.writeBuffer(&headers1)
         
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
-        
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
+
         // should return nothing thus far and wait for CONTINUATION frames and an END_HEADERS flag
         decoder.append(bytes: buf)
         XCTAssertNil(try decoder.nextFrame())
@@ -1740,8 +1842,12 @@ class HTTP2FrameParserTests: XCTestCase {
         var buf = byteBuffer(withBytes: frameBytes, extraCapacity: 10)
         buf.writeBuffer(&headers1)
         
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
-        
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
+
         // should return nothing thus far and wait for CONTINUATION frames and an END_HEADERS flag
         decoder.append(bytes: buf)
         XCTAssertNil(try decoder.nextFrame())
@@ -1777,7 +1883,11 @@ class HTTP2FrameParserTests: XCTestCase {
         var buf = byteBuffer(withBytes: frameBytes, extraCapacity: 10)
         buf.writeBuffer(&headers)
 
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
 
         // should throw
         decoder.append(bytes: buf)
@@ -1799,7 +1909,11 @@ class HTTP2FrameParserTests: XCTestCase {
         var buf = byteBuffer(withBytes: frameBytes, extraCapacity: 10)
         buf.writeBuffer(&headers1)
 
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
 
         // should return nothing thus far and wait for CONTINUATION frames and an END_HEADERS flag
         decoder.append(bytes: buf)
@@ -1836,7 +1950,11 @@ class HTTP2FrameParserTests: XCTestCase {
         var buf = byteBuffer(withBytes: frameBytes, extraCapacity: 10)
         buf.writeBuffer(&headers1)
 
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
 
         // should return nothing thus far and wait for CONTINUATION frames and an END_HEADERS flag
         decoder.append(bytes: buf)
@@ -1878,7 +1996,11 @@ class HTTP2FrameParserTests: XCTestCase {
         buf.writeBuffer(&headers1)
         buf.writeRepeatingByte(0, count: 2)
 
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
 
         // should return nothing thus far and wait for CONTINUATION frames and an END_HEADERS flag
         decoder.append(bytes: buf)
@@ -1923,7 +2045,11 @@ class HTTP2FrameParserTests: XCTestCase {
         buf.writeBuffer(&headers1)
         buf.writeRepeatingByte(0, count: 5)
 
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
 
         // should return nothing thus far and wait for CONTINUATION frames and an END_HEADERS flag
         decoder.append(bytes: buf)
@@ -1947,7 +2073,62 @@ class HTTP2FrameParserTests: XCTestCase {
 
         self.assertEqualFrames(frame, expectedFrame)
     }
-    
+
+    func testMaximumSequentialContinuationFrames() throws {
+        let continuationBytes: [UInt8] = [
+            // 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 (none)
+            0x00, 0x00, 0x00, 0x03,     // 4-byte stream identifier
+        ]
+
+        var frameBytes: [UInt8] = [
+            // HEADERS
+            0x00, 0x00, 0x01,           // 3-byte payload length (1 byte)
+            0x01,                       // 1-byte frame type (HEADERS)
+            0x08,                       // 1-byte flags (PADDED)
+            0x00, 0x00, 0x00, 0x03,     // 4-byte stream identifier
+            0x00,                       // 1-byte padding length (0)
+            // CONTINUATION frame with the END_HEADERS flag set
+            0x00, 0x00, 0x00,           // 3-byte payload length (0 bytes)
+            0x09,                       // 1-byte frame type (CONTINUATION)
+            0x04,                       // 1-byte flags (END_HEADERS)
+            0x00, 0x00, 0x00, 0x03      // 4-byte stream identifier
+        ]
+
+        let excessContinuationFrames = 1
+
+        // Iteratively test that sequential CONTINUATION frames are received up to the configured
+        // limit, after which an error should be thrown.
+        for numberOfContinuationFrames in 1 ... self.maximumSequentialContinuationFrames + excessContinuationFrames {
+            let buf = byteBuffer(withBytes: frameBytes)
+
+            var decoder = HTTP2FrameDecoder(
+                allocator: self.allocator,
+                expectClientMagic: false,
+                maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+            )
+            decoder.append(bytes: buf)
+
+            if numberOfContinuationFrames <= self.maximumSequentialContinuationFrames {
+                let expectedFrame = HTTP2Frame(
+                    streamID: HTTP2StreamID(3),
+                    payload: .headers(.init(headers: [:], endStream: false, paddingBytes: 0))
+                )
+                try assertReadsFrame(from: buf, matching: expectedFrame)
+            } else {
+                XCTAssertThrowsError(try decoder.nextFrame(), "Should throw an 'Excessive CONTINUATION frames' error") { err in
+                    XCTAssert(err is NIOHTTP2Errors.ExcessiveContinuationFrames)
+                }
+            }
+
+            // The CONTINUATION frame with the END_HEADERS flag not set will be inserted just before
+            // the CONTINUATION frame with the END_HEADERS flag set.
+            frameBytes.insert(contentsOf: continuationBytes, at: frameBytes.endIndex - continuationBytes.count)
+        }
+    }
+
     // MARK: - ALTSVC frames
     
     func testAltServiceFrameDecoding() throws {
@@ -1989,8 +2170,12 @@ class HTTP2FrameParserTests: XCTestCase {
         buf.writeBytes(field.readableBytesView)
         XCTAssertEqual(buf.readableBytes, 30)
         
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
-        
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
+
         // cannot have origin on a non-root stream
         buf.setInteger(UInt8(1), at: 8)
         decoder.append(bytes: buf)
@@ -2042,7 +2227,11 @@ class HTTP2FrameParserTests: XCTestCase {
             0x00, 0x09,                 // 2-byte origin length
         ]
         let buffer = self.allocator.buffer(bytes: frameBytes)
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
         decoder.append(bytes: buffer)
 
         XCTAssertThrowsError(try decoder.nextFrame(), "Should throw a frame size error") { err in
@@ -2093,8 +2282,12 @@ class HTTP2FrameParserTests: XCTestCase {
         }
         XCTAssertEqual(buf.readableBytes, 51)
         
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
-        
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
+
         // MUST be sent on root stream (else ignored)
         buf.setInteger(UInt8(1), at: 8)
         decoder.append(bytes: buf)
@@ -2179,7 +2372,11 @@ class HTTP2FrameParserTests: XCTestCase {
         buf.writeBuffer(&headers4)
         
         // This should now yield a HEADERS frame containing the complete set of headers
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
         decoder.append(bytes: buf)
         let frame: HTTP2Frame! = try decoder.nextFrame()?.0
         XCTAssertNotNil(frame)
@@ -2226,7 +2423,11 @@ class HTTP2FrameParserTests: XCTestCase {
         buf.writeBuffer(&headers4)
         
         // This should now yield a HEADERS frame containing the complete set of headers
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
         decoder.append(bytes: buf)
         let frame: HTTP2Frame! = try decoder.nextFrame()?.0
         XCTAssertNotNil(frame)
@@ -2295,7 +2496,11 @@ class HTTP2FrameParserTests: XCTestCase {
         let dataFrame = HTTP2Frame(streamID: streamID, payload: .data(.init(data: .byteBuffer(dataBuf), endStream: true)))
         let goawayFrame = HTTP2Frame(streamID: .rootStream, payload: .goAway(lastStreamID: streamID, errorCode: .noError, opaqueData: dataBuf))
         
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
         decoder.append(bytes: buf)
         
         let frame1 = try decoder.nextFrame()?.0
@@ -2331,7 +2536,11 @@ class HTTP2FrameParserTests: XCTestCase {
         var buf = byteBuffer(withBytes: frameBytes, extraCapacity: payload.readableBytes)
         buf.writeBytes(payload.readableBytesView)
 
-        var decoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
+        var decoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: self.maximumSequentialContinuationFrames
+        )
         var slice = buf.readSlice(length: 10)!
         decoder.append(bytes: slice)
         guard let result = try assertNoThrowWithValue(decoder.nextFrame()) else {
diff --git a/Tests/NIOHTTP2Tests/HTTP2InlineStreamMultiplexerTests.swift b/Tests/NIOHTTP2Tests/HTTP2InlineStreamMultiplexerTests.swift
index c077aab..68b1f3e 100644
--- a/Tests/NIOHTTP2Tests/HTTP2InlineStreamMultiplexerTests.swift
+++ b/Tests/NIOHTTP2Tests/HTTP2InlineStreamMultiplexerTests.swift
@@ -36,7 +36,11 @@ typealias IODataWriteRecorder = WriteRecorder<IOData>
 
 extension IODataWriteRecorder {
     func drainConnectionSetupWrites(mode: NIOHTTP2Handler.ParserMode = .server) throws {
-        var frameDecoder = HTTP2FrameDecoder(allocator: ByteBufferAllocator(), expectClientMagic: mode == .client)
+        var frameDecoder = HTTP2FrameDecoder(
+            allocator: ByteBufferAllocator(),
+            expectClientMagic: mode == .client,
+            maximumSequentialContinuationFrames: 5
+        )
 
         while self.flushedWrites.count > 0 {
             let write = self.flushedWrites.removeFirst()
@@ -287,7 +291,11 @@ final class HTTP2InlineStreamMultiplexerTests: XCTestCase {
         let expectedFrames = streamIDs.map { HTTP2Frame(streamID: $0, payload: .rstStream(.cancel)) }
         XCTAssertEqual(expectedFrames.count, frameReceiver.flushedWrites.count)
 
-        var frameDecoder = HTTP2FrameDecoder(allocator: channel.allocator, expectClientMagic: false)
+        var frameDecoder = HTTP2FrameDecoder(
+            allocator: channel.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: 5
+        )
         for (idx, expectedFrame) in expectedFrames.enumerated() {
             if case .byteBuffer(let flushedWriteBuffer) = frameReceiver.flushedWrites[idx] {
                 frameDecoder.append(bytes: flushedWriteBuffer)
@@ -328,7 +336,11 @@ final class HTTP2InlineStreamMultiplexerTests: XCTestCase {
         // Now we close it. This triggers a RST_STREAM frame.
         childChannel.close(promise: nil)
         XCTAssertEqual(frameReceiver.flushedWrites.count, 1)
-        var frameDecoder = HTTP2FrameDecoder(allocator: channel.allocator, expectClientMagic: false)
+        var frameDecoder = HTTP2FrameDecoder(
+            allocator: channel.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: 5
+        )
         if case .byteBuffer(let flushedWriteBuffer) = frameReceiver.flushedWrites[0] {
             frameDecoder.append(bytes: flushedWriteBuffer)
         }
@@ -369,7 +381,11 @@ final class HTTP2InlineStreamMultiplexerTests: XCTestCase {
         let closed = ManagedAtomic<Bool>(false)
         childChannel.close().whenComplete { _ in closed.store(true, ordering: .sequentiallyConsistent) }
         XCTAssertEqual(frameReceiver.flushedWrites.count, 1)
-        var frameDecoder = HTTP2FrameDecoder(allocator: channel.allocator, expectClientMagic: false)
+        var frameDecoder = HTTP2FrameDecoder(
+            allocator: channel.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: 5
+        )
         if case .byteBuffer(let flushedWriteBuffer) = frameReceiver.flushedWrites[0] {
             frameDecoder.append(bytes: flushedWriteBuffer)
         }
@@ -426,7 +442,11 @@ final class HTTP2InlineStreamMultiplexerTests: XCTestCase {
             thirdClosed.store(true, ordering: .sequentiallyConsistent)
         }
         XCTAssertEqual(frameReceiver.flushedWrites.count, 1)
-        var frameDecoder = HTTP2FrameDecoder(allocator: channel.allocator, expectClientMagic: false)
+        var frameDecoder = HTTP2FrameDecoder(
+            allocator: channel.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: 5
+        )
         if case .byteBuffer(let flushedWriteBuffer) = frameReceiver.flushedWrites[0] {
             frameDecoder.append(bytes: flushedWriteBuffer)
         }
@@ -470,7 +490,11 @@ final class HTTP2InlineStreamMultiplexerTests: XCTestCase {
             }
         }
         XCTAssertEqual(frameReceiver.flushedWrites.count, 1)
-        var frameDecoder = HTTP2FrameDecoder(allocator: channel.allocator, expectClientMagic: false)
+        var frameDecoder = HTTP2FrameDecoder(
+            allocator: channel.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: 5
+        )
         if case .byteBuffer(let flushedWriteBuffer) = frameReceiver.flushedWrites[0] {
             frameDecoder.append(bytes: flushedWriteBuffer)
         }
@@ -576,7 +600,11 @@ final class HTTP2InlineStreamMultiplexerTests: XCTestCase {
         XCTAssertEqual(frameRecorder.receivedFrames.count, 0)
         XCTAssertFalse(childChannel.isActive)
         XCTAssertEqual(writeRecorder.flushedWrites.count, 1)
-        var frameDecoder = HTTP2FrameDecoder(allocator: channel.allocator, expectClientMagic: false)
+        var frameDecoder = HTTP2FrameDecoder(
+            allocator: channel.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: 5
+        )
         if case .byteBuffer(let flushedWriteBuffer) = writeRecorder.flushedWrites[0] {
             frameDecoder.append(bytes: flushedWriteBuffer)
         }
diff --git a/Tests/NIOHTTP2Tests/SimpleClientServerFramePayloadStreamTests.swift b/Tests/NIOHTTP2Tests/SimpleClientServerFramePayloadStreamTests.swift
index 4e8d814..db28c57 100644
--- a/Tests/NIOHTTP2Tests/SimpleClientServerFramePayloadStreamTests.swift
+++ b/Tests/NIOHTTP2Tests/SimpleClientServerFramePayloadStreamTests.swift
@@ -201,13 +201,16 @@ class SimpleClientServerFramePayloadStreamTests: XCTestCase {
     func basicHTTP2Connection(clientSettings: HTTP2Settings = nioDefaultSettings,
                               serverSettings: HTTP2Settings = nioDefaultSettings,
                               maximumBufferedControlFrames: Int = 10000,
+                              maximumSequentialContinuationFrames: Int = 5,
                               withMultiplexerCallback multiplexerCallback: NIOChannelInitializer? = nil) throws {
         XCTAssertNoThrow(try self.clientChannel.pipeline.addHandler(NIOHTTP2Handler(mode: .client,
                                                                                     initialSettings: clientSettings,
-                                                                                    maximumBufferedControlFrames: maximumBufferedControlFrames)).wait())
+                                                                                    maximumBufferedControlFrames: maximumBufferedControlFrames,
+                                                                                    maximumSequentialContinuationFrames: maximumSequentialContinuationFrames)).wait())
         XCTAssertNoThrow(try self.serverChannel.pipeline.addHandler(NIOHTTP2Handler(mode: .server,
                                                                                     initialSettings: serverSettings,
-                                                                                    maximumBufferedControlFrames: maximumBufferedControlFrames)).wait())
+                                                                                    maximumBufferedControlFrames: maximumBufferedControlFrames,
+                                                                                    maximumSequentialContinuationFrames: maximumSequentialContinuationFrames)).wait())
 
         if let multiplexerCallback = multiplexerCallback {
             XCTAssertNoThrow(try self.clientChannel.pipeline.addHandler(HTTP2StreamMultiplexer(mode: .client,
@@ -1706,7 +1709,9 @@ class SimpleClientServerFramePayloadStreamTests: XCTestCase {
 
     func testForbidsExceedingMaxHeaderListSizeBeforeDecoding() throws {
         // Begin by getting the connection up.
-        try self.basicHTTP2Connection()
+        // Ensure that the limit for the number of sequential CONTINUATION frames is enough to
+        // accommodate exceeding the set max header list size.
+        try self.basicHTTP2Connection(maximumSequentialContinuationFrames: 225)
 
         // The server is going to shrink its value for max header list size.
         let newSettings = [HTTP2Setting(parameter: .maxHeaderListSize, value: 225)]
@@ -1789,6 +1794,63 @@ class SimpleClientServerFramePayloadStreamTests: XCTestCase {
         try self.clientChannel.assertReceivedFrame().assertGoAwayFrame(lastStreamID: .maxID, errorCode: UInt32(HTTP2ErrorCode.protocolError.networkCode), opaqueData: nil)
     }
 
+    func testForbidsExceedingMaximumSequentialContinuationFrames() throws {
+        let maximumSequentialContinuationFrames = 5
+
+        // Begin by getting the connection up.
+        try self.basicHTTP2Connection(
+            maximumSequentialContinuationFrames: maximumSequentialContinuationFrames
+        )
+
+        let headersFrame: [UInt8] = [
+            0x00, 0x00, 0x00,           // 3-byte payload length (0 bytes)
+            0x01,                       // 1-byte frame type (HEADERS)
+            0x00,                       // 1-byte flags (none)
+            0x00, 0x00, 0x00, 0x03,     // 4-byte stream identifier
+        ]
+        let continuationFrame: [UInt8] = [
+            0x00, 0x00, 0x00,           // 3-byte payload length (0 bytes)
+            0x09,                       // 1-byte frame type (CONTINUATION)
+            0x00,                       // 1-byte flags (none)
+            0x00, 0x00, 0x00, 0x03      // 4-byte stream identifier
+        ]
+
+        var firstBuffer = self.serverChannel.allocator.buffer(capacity: 128)
+        firstBuffer.writeBytes(headersFrame)
+        for _ in 0..<maximumSequentialContinuationFrames {
+            firstBuffer.writeBytes(continuationFrame)
+        }
+
+        // Sending this should not result in an error.
+        XCTAssertNoThrow(try self.serverChannel.writeInbound(firstBuffer))
+        XCTAssertNoThrow(XCTAssertNil(try self.serverChannel.readOutbound(as: ByteBuffer.self)))
+
+        // But if we send one more frame, that will be treated as an excess CONTINUATION frame.
+        XCTAssertThrowsError(
+            try self.serverChannel.writeInbound(
+                firstBuffer.getSlice(at: firstBuffer.writerIndex - 9, length: 9)!
+            )
+        ) { error in
+            XCTAssertEqual(
+                error as? NIOHTTP2Errors.ExcessiveContinuationFrames,
+                NIOHTTP2Errors.excessiveContinuationFrames()
+            )
+        }
+        guard let responseFrame = try assertNoThrowWithValue(
+            self.serverChannel.readOutbound(as: ByteBuffer.self)
+        ) else {
+            XCTFail("Did not receive response frame")
+            return
+        }
+        XCTAssertNoThrow(try self.clientChannel.writeInbound(responseFrame))
+
+        try self.clientChannel.assertReceivedFrame().assertGoAwayFrame(
+            lastStreamID: .maxID,
+            errorCode: UInt32(HTTP2ErrorCode.enhanceYourCalm.networkCode),
+            opaqueData: nil
+        )
+    }
+
     func testNoStreamWindowUpdateOnEndStreamFrameFromServer() throws {
         let maxFrameSize = (1 << 24) - 1  // Max value of SETTINGS_MAX_FRAME_SIZE is 2^24-1.
         let clientSettings = nioDefaultSettings + [HTTP2Setting(parameter: .maxFrameSize, value: maxFrameSize)]
diff --git a/Tests/NIOHTTP2Tests/TestUtilities.swift b/Tests/NIOHTTP2Tests/TestUtilities.swift
index 0f13cc8..52c0bbe 100644
--- a/Tests/NIOHTTP2Tests/TestUtilities.swift
+++ b/Tests/NIOHTTP2Tests/TestUtilities.swift
@@ -263,7 +263,11 @@ extension EmbeddedChannel {
     func decodedSentFrames(file: StaticString = #filePath, line: UInt = #line) throws -> [HTTP2Frame] {
         var receivedFrames: [HTTP2Frame] = Array()
 
-        var frameDecoder = HTTP2FrameDecoder(allocator: self.allocator, expectClientMagic: false)
+        var frameDecoder = HTTP2FrameDecoder(
+            allocator: self.allocator,
+            expectClientMagic: false,
+            maximumSequentialContinuationFrames: 5
+        )
         while let buffer = try assertNoThrowWithValue(self.readOutbound(as: ByteBuffer.self), file: (file), line: line) {
             frameDecoder.append(bytes: buffer)
             if let (frame, _) = try frameDecoder.nextFrame() {