HPACKDecoder: reject more than 2 consecutive dynamic-table-size updates per header block (RFC 7541 §6.3)
- Dominant language
- Swift
- Stars
- 503
- Forks
- 103
- Avg merge
- 18h 8m
- Merged PRs (30d)
- 4
Description
### Summary
`HPACKDecoder.swift`'s `decodeHeaders(from:)` loop accepts an unlimited number of consecutive dynamic-table-size updates (`0x20` bytes) at the start of a header block. RFC 7541 §6.3 and RFC 9113 §4.3.1 together imply at most two table-size updates per header block (one to signal the new minimum, one for the new maximum, when the encoder acknowledges a SETTINGS change).
Current code (`HPACKDecoder.swift:126-137`):
```swift
while buffer.readableBytes > 0 {
switch try self.decodeHeader(from: &buffer) {
case .tableSizeChange:
guard headers.count == 0 else {
throw NIOHPACKErrors.IllegalDynamicTableSizeChange()
}
// No count limit — unlimited size updates accepted at block start
case .header(let header):
listSize += header.size
headers.append(header)
}
}
```
The `guard headers.count == 0` correctly prevents size updates after the first real header, but places no upper bound on the number of updates at the block start. A header block consisting of thousands of `0x20` bytes followed by a single indexed header is accepted.
Note on severity: each `0x20` sets the table size to 0, and on an already-empty table the eviction loop is O(1), so there is no meaningful CPU amplification. This is a spec-conformance issue, not a DoS vulnerability.
### Proposed fix
```swift
var tableSizeUpdates = 0
while buffer.readableBytes > 0 {
switch try self.decodeHeader(from: &buffer) {
case .tableSizeChange:
guard headers.count == 0 else {
throw NIOHPACKErrors.IllegalDynamicTableSizeChange()
}
tableSizeUpdates += 1
guard tableSizeUpdates <= 2 else {
throw NIOHPACKErrors.IllegalDynamicTableSizeChange()
}
case .header(let header):
listSize += header.size
headers.append(header)
}
}
```
### Reproducer
```swift
import NIOCore
import XCTest
@testable import NIOHPACK
final class HPACKTableSizeUpdateCountTests: XCTestCase {
func testThreeConsecutiveUpdatesAccepted() throws {
var decoder = HPACKDecoder(allocator: .init())
// Three 0x20 bytes (table-size-update to 0) + one indexed header (0x82 = :method GET)
var buf = ByteBuffer(bytes: [0x20, 0x20, 0x20, 0x82])
// Currently succeeds; should throw after the 3rd update:
let headers = try decoder.decodeHeaders(from: &buf)
XCTAssertFalse(headers.isEmpty)
}
}
```
### References
- RFC 7541 §6.3: https://www.rfc-editor.org/rfc/rfc7541#section-6.3
- RFC 9113 §4.3.1: https://www.rfc-editor.org/rfc/rfc9113#section-4.3.1
Contributor guide
Research direction
Start in HPACKDecoder.swift at decodeHeaders(from:), especially lines 126-137, and review the existing HPACK decoder tests. Add a regression test based on the provided three-update reproducer, then run the relevant test target. Done means a header block with three consecutive table-size updates is rejected while valid blocks with up to two updates continue to work.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- swift
- Domain
- networking
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100