apple / apple/swift-nio-http2

HPACKDecoder: reject more than 2 consecutive dynamic-table-size updates per header block (RFC 7541 §6.3)

Open Beginner friendly
#550 3 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.