Skip to content

Commit 2a044b9

Browse files
HPACKDecoder: reject more than two table size updates per header block (#551)
RFC 7541 § 4.2 specifies that an encoder emits at most two dynamic table size updates between two header blocks (the smallest size reached, then the final size). The decoder enforced placement - updates must precede the first header - but put no upper bound on their count, so a block of arbitrarily many 0x20 bytes followed by a header was accepted. This is a conformance gap. It is not a DoS: setting an already-empty table to 0 is O(1). Modifications: - Count consecutive table size updates in decodeHeaders(from:) and throw IllegalDynamicTableSizeChange once a third is seen. - Extend the existing § 4.2 comment to cite the at-most-two rule. - Add tests covering the two-accepted and three-rejected cases. Result: Header blocks with more than two dynamic table size updates are rejected as malformed, matching RFC 7541 § 4.2. Co-authored-by: George Barnett <gbarnett@apple.com>
1 parent 634d078 commit 2a044b9

2 files changed

Lines changed: 35 additions & 0 deletions

File tree

Sources/NIOHPACK/HPACKDecoder.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,19 +122,32 @@ public struct HPACKDecoder: Sendable {
122122
headers.reserveCapacity(16)
123123

124124
var listSize = 0
125+
var tableSizeUpdates = 0
125126

126127
while buffer.readableBytes > 0 {
127128
switch try self.decodeHeader(from: &buffer) {
128129
case .tableSizeChange:
129130
// RFC 7541 § 4.2 <https://httpwg.org/specs/rfc7541.html#maximum.table.size>:
130131
//
132+
// 1. "In the case that this size is changed more than once in [the interval between two
133+
// header blocks], the smallest maximum table size that occurs in that interval MUST be
134+
// signaled in a dynamic table size update. The final maximum size is always signaled,
135+
// resulting in at most two dynamic table size updates."
136+
//
131137
// 2. "This dynamic table size update MUST occur at the beginning of the first header block
132138
// following the change to the dynamic table size."
133139
guard headers.count == 0 else {
134140
// If our decode buffer has any data in it, then this is out of place.
135141
// Treat it as an invalid input
136142
throw NIOHPACKErrors.IllegalDynamicTableSizeChange()
137143
}
144+
145+
// A conformant encoder emits at most two consecutive table size updates (see point 1
146+
// above). More than two is never valid, so reject the block as malformed.
147+
tableSizeUpdates += 1
148+
guard tableSizeUpdates <= 2 else {
149+
throw NIOHPACKErrors.IllegalDynamicTableSizeChange()
150+
}
138151
case .header(let header):
139152
listSize += header.size
140153
guard listSize <= self.maxHeaderListSize else {

Tests/NIOHPACKTests/HPACKCodingTests.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,28 @@ class HPACKCodingTests: XCTestCase {
551551
}
552552
}
553553

554+
func testAcceptsTwoConsecutiveTableSizeUpdates() throws {
555+
// Two dynamic-table-size updates (0x20 == update to 0) at the start of a header block, followed
556+
// by an indexed header field (0x82 == static index 2, ":method: GET"). RFC 7541 § 4.2 permits up
557+
// to two table size updates per header block, so this must decode successfully.
558+
var request = buffer(wrapping: [0x20, 0x20, 0x82])
559+
var decoder = HPACKDecoder(allocator: ByteBufferAllocator())
560+
561+
let decoded = try decoder.decodeHeaders(from: &request)
562+
XCTAssertEqual(decoded, HPACKHeaders([(":method", "GET")]))
563+
}
564+
565+
func testRejectsMoreThanTwoConsecutiveTableSizeUpdates() throws {
566+
// Three dynamic-table-size updates (0x20) before the first header. RFC 7541 § 4.2 caps a header
567+
// block at two table size updates, so the decoder must reject the block as malformed.
568+
var request = buffer(wrapping: [0x20, 0x20, 0x20, 0x82])
569+
var decoder = HPACKDecoder(allocator: ByteBufferAllocator())
570+
571+
XCTAssertThrowsError(try decoder.decodeHeaders(from: &request)) { error in
572+
XCTAssertTrue(error is NIOHPACKErrors.IllegalDynamicTableSizeChange)
573+
}
574+
}
575+
554576
func testHPACKHeadersDescription() throws {
555577
let headerList1: [(String, String)] = [
556578
(":method", "GET"),

0 commit comments

Comments
 (0)