feat(core): harden HTTP/2 abuse resistance

This commit is contained in:
Zakaria El Orche
2026-08-13 19:40:52 +00:00
parent ee90ac44ff
commit 5755ef77fe
18 changed files with 793 additions and 30 deletions
+28 -6
View File
@@ -982,21 +982,43 @@ window and pool byte capacity together; never raise credit independently of boun
---
## DEC-29 — Keep HTTP/2 opt-in until the adversarial phase is complete
## DEC-29 — Keep HTTP/2 opt-in through the cleartext rollout boundary
**Context.** Trailers and push streaming make the protocol feature-complete for ordinary and gRPC-
shaped traffic, but the dedicated rate-based and composite abuse controls are deliberately owned
by the following security phase.
**Decision.** Keep `FlashConfiguration.http2Enabled` defaulting to `false` during this phase.
Applications can enable the complete path explicitly; the default changes only after the hostile-
peer suite and its limits are green.
**Decision.** Keep `FlashConfiguration.http2Enabled` defaulting to `false`. Applications can
enable the complete path explicitly. The Phase 13 hostile-peer suite is now green, but the same
flag currently also admits cleartext prior-knowledge traffic; Phase 14 owns splitting that into a
separate `http2CleartextEnabled` opt-in before the general protocol default can change safely.
**Consequence.** Existing deployments do not silently expose a newly completed protocol before its
adversarial gate. This is rollout sequencing, not an architectural separation: both protocols use
the same public request/response, header, trailer and streaming APIs.
**Revisit when.** At Phase 13 closure; either flip the default with evidence or record why it must
remain opt-in.
**Revisit when.** At Phase 14 closure, after TLS HTTP/2 and cleartext h2c have independent rollout
controls.
---
## DEC-30 — Rate-limit aggregate non-progress work as one class
**Context.** SETTINGS, PING, PRIORITY, WINDOW_UPDATE, empty DATA and unknown extension frames have
different wire semantics but share the abuse property that they can consume parser/control work
without advancing an application message. Separate limits leave gaps when an attacker alternates
frame types below every individual threshold.
**Decision.** Keep dedicated lower limits for mandatory SETTINGS and PING replies, plus one
connection-owned two-bucket counter for the aggregate non-progress class. RST_STREAM and stream
creation retain dedicated CVE-2023-44487 counters because their expensive effect is stream
lifecycle churn, not merely frame parsing.
**Consequence.** Mixed floods are bounded without six timers or maps. All counters are fixed fields
on the connection, use `System.nanoTime()`, allocate nothing per increment and require no reaper
thread. A fixed control-intent pool and one-in-flight intent per live stream bound write queues.
**Revisit when.** Production telemetry shows legitimate control-heavy traffic approaching the
aggregate default; tune the threshold from evidence without splitting the defence by frame type.
---
+25 -4
View File
@@ -74,7 +74,7 @@ Status values: `not started` / `in progress` / `blocked` / `done`.
| 10 — Stream state machine + dispatch | done | `feature/core/http2` | Explicit stream transition table, bounded primitive stream table and pool, pseudo-header/message validation, protocol-neutral `Request` assembly, virtual-thread dispatch and exception path, cancellation-safe release, raw h2c + Java HTTP/2 integration. Phase 11 closed the two deferred content-length/DATA cases; h2spec sections 5/8 are now 39/39. JMH pooled lifecycle: 458.499 ns/op, 0.003 B/op, no GC. 618/618 tests green at phase closure. |
| 11 — DATA, flow control, bodies | done | `feature/core/http2` | Two-level receive/send flow control, consumption-driven WINDOW_UPDATE hysteresis, bounded/coalescing DATA pool, inline and blocking streaming request bodies through the existing `RequestBody`, resumable fixed/known/unknown response streams, content-length and empty-DATA validation. Real TLS HTTP/2 transfer: 100 MiB upload + 100 MiB download verified byte-for-byte. h2spec combined sections 5, 6.1, 6.9 and 8: 50 passed, 1 tool-skipped, 0 failed. JMH: inline materialization exactly one 1,040-byte array; request streaming 0.001 B/op; response streaming 0.002 B/op; full pooled lifecycle 0.003 B/op. 633/633 tests green from a clean `-Pjmh` build. |
| 12 — Trailers, half-close, gRPC | done | `feature/core/http2` | Protocol-neutral request/response trailers, bounded push streaming, four half-close orderings and authority-form CONNECT tunnels complete. Real grpcurl 1.9.3 unary/server-streaming/error interop passes. EX-48/49 fixed. HTTP/2 remains opt-in until the Phase 13 hostile-peer gate (DEC-29). 649/649 tests green from a clean `-Pjmh` build. |
| 13 — Security hardening & abuse resistance | not started | — | — |
| 13 — Security hardening & abuse resistance | done | `feature/core/http2` | Two-bucket Rapid Reset/stream/settings/ping/aggregate counters, control/write queue bounds, optional stream/byte/lifetime budgets, absolute header and idle-stream deadlines, and hostile-peer suite complete. Security review found+fixed EX-50/51. JMH counter: 38.083 ns/op, ~10^-4 B/op, no GC. 100k-CONTINUATION attack terminates in under 2 s with bounded retained heap. 663/663 tests green from a clean `-Pjmh` build. |
| 14 — h2c prior knowledge + proxy support | not started | — | — |
| 15 — RFC 8441 extended CONNECT (WS over h2) | not started | — | — |
| 16 — Compliance test suite | not started | — | — |
@@ -794,6 +794,25 @@ targets and HTTP/2 `:authority` arrive without that prefix, so the existing CONN
never match its documented target. **Fix**: normalize CONNECT authority targets separately in the
shared router registration path and verify a live bidirectional HTTP/2 tunnel. **Phase**: 12.
### EX-50 — Declared HTTP/2 header and stream idle deadlines were not enforced
Found during the whole-package hostile-peer review. `HEADER_BLOCK_ASSEMBLY_TIMEOUT_MS` and
`STREAM_IDLE_TIMEOUT_MS` existed in `Http2Limits` and were described as enforced defences, but no
production path read either constant. A peer could retain a CONTINUATION assembly or an open
stream indefinitely. **Fix**: give header assembly an absolute non-renewable deadline checked on
frames and read wakeups; track per-stream activity and cancel idle streams with `RST_STREAM
CANCEL`; expose the stream deadline operationally and add deadline regression tests. **Phase**: 13.
### EX-51 — Concurrent half-close could retire the same pooled HTTP/2 stream twice
Found when the clean integration suite logged an internal error despite passing its assertions.
The demultiplexer and response-completion thread could both observe a closed stream, then one
thread could recycle it before the other read its id. The loser attempted to remove stream id
zero; a more unfortunate interleaving could have touched a reused pooled object. **Fix**: make
stream retirement atomic in `Http2StreamTable` and require both the expected stream id and object
identity to match the live table entry. A regression test proves that a stale retirement cannot
remove the next generation of the same pooled object. **Phase**: 13.
---
# PART III — The phases
@@ -2834,9 +2853,11 @@ of allocated memory (assert with a heap sample, not a hope).
applicable, and how to tune it. This is the document an operator reads at 3 a.m.
### DoD
- [ ] Every attack in this phase has a test that proves the defence.
- [ ] Every limit is documented with its rationale.
- [ ] A `security-review` pass over the whole `h2` package is completed and its findings fixed.
- [x] Every attack in this phase has a test that proves the defence.
- [x] Every limit is documented with its rationale.
- [x] A `security-review` pass over the whole `h2` package is completed and its findings fixed
(`EX-50`: declared header-assembly and idle-stream deadlines were not wired; `EX-51`:
concurrent half-close could retire the same pooled stream twice).
---
+42
View File
@@ -0,0 +1,42 @@
# HTTP/2 security controls
HTTP/2 multiplexing lets one connection create disproportionate parser, stream and response work.
Flash therefore combines structural bounds, flow-control bounds and rate bounds. Rate counters use
two fixed half-window buckets, allocate nothing per frame and need no timer thread.
JMH on JDK 21 measures one rate-counter increment at 38.083 ns/op and approximately
`10^-4 B/op` (allocation noise floor, no GC).
| Limit | Default | Defence / tuning guidance |
|---|---:|---|
| `MAX_CONCURRENT_STREAMS` | 64 | Bounds simultaneously retained stream state. |
| `MAX_STREAMS_CREATED_PER_INTERVAL` | 400 / 10 s | Companion to Rapid Reset; tune with `h2MaxStreamsCreatedPerInterval`. |
| `MAX_RESET_STREAMS_PER_INTERVAL` | 200 / 10 s | CVE-2023-44487 Rapid Reset; tune with `h2MaxResetStreamsPerInterval`. |
| `MAX_CONTINUATION_FRAMES_PER_BLOCK` | 8 | CVE-2024-27316 CONTINUATION flood. |
| `MAX_HEADER_LIST_SIZE` | 32 KiB | Stops HPACK expansion before fields reach stream storage. |
| `MAX_HPACK_STRING_LENGTH` | 8 KiB | Bounds one decoded literal, including Huffman expansion. |
| `MAX_SETTINGS_PER_INTERVAL` | 100 / 10 s | Bounds mandatory SETTINGS acknowledgements. |
| `MAX_PINGS_PER_INTERVAL` | 200 / 10 s | Bounds mandatory PING acknowledgements. |
| `MAX_USELESS_FRAMES_PER_INTERVAL` | 10,000 / 10 s | Aggregate CPU bound for PRIORITY, WINDOW_UPDATE, empty DATA and unknown frames. |
| `MAX_SETTINGS_ACK_QUEUE_DEPTH` | 64 | Bounds queued SETTINGS control writes. |
| `MAX_PING_QUEUE_DEPTH` | 64 | Bounds queued PING control writes. |
| `MAX_EMPTY_DATA_FRAMES_PER_STREAM` | 1,000 | Stops DATA work that spends no flow-control credit. |
| `INITIAL_WINDOW_SIZE_LOCAL` | 1 MiB | Matches the bounded DATA pool; consumption, not receipt, returns credit. |
| `MAX_REQUEST_BODY_SIZE` | 100 MiB | Hard per-stream request body bound. |
| `HEADER_BLOCK_ASSEMBLY_TIMEOUT_MS` | 10 s | Absolute HEADERS-to-END_HEADERS deadline. |
| `STREAM_IDLE_TIMEOUT_MS` | 60 s | Cancels retained inactive streams; tune with `h2StreamIdleTimeoutMs`. |
| `FRAME_READ_TIMEOUT_MS` | 20 s | Absolute partial-frame deadline. |
| `WRITE_TIMEOUT_MS` | 30 s | Interrupts a socket writer blocked by a peer that stopped reading. |
| `MAX_STREAMS_PER_CONNECTION` | 100,000 | Optional connection churn budget; zero disables, tune with `h2MaxStreamsPerConnection`. |
| `MAX_BYTES_PER_CONNECTION` | disabled | Optional wire-byte budget; tune with `h2MaxBytesPerConnection`. |
| `MAX_CONNECTION_LIFETIME_MS` | disabled | Optional lifetime rotation; tune with `h2MaxConnectionLifetimeMs`. |
`h2AbuseRateIntervalMs` changes the rolling interval for reset and stream-creation operator
limits. Breaching a connection-wide rate or budget produces GOAWAY `ENHANCE_YOUR_CALM`; malformed
stream-local messages use the RFC-defined stream error. The ordinary write queue is bounded by the
64 live streams and their single-in-flight response intent; control writes use the fixed scratch
slots above, so a slow reader cannot create an unbounded application queue.
The security suite covers Rapid Reset, stream churn, SETTINGS/PING/non-progress floods, 100,000
CONTINUATION frames, HPACK expansion, malformed names/pseudo-fields, configurable resource budgets,
header assembly deadlines and idle-stream cancellation. HTTP/1 request/header/body security tests
remain in the full suite and the shared public message model uses the same bounds on both paths.