Files
Flash5/flash/docs/http2/HTTP1-HARDENING.md
T
Zakaria El OrcheandClaude Sonnet 5 5a2aaf5a07 feat(core): HTTP/2 Phase 1 — HTTP/1.1 hardening and protocol negotiation
Fixes the request-smuggling and resource-exhaustion debt in the existing
HTTP/1.1 parser, and adds the ALPN/h2c-preface negotiation seam so a
connection's protocol is decided once, before any request is parsed, per
flash/docs/http2/IMPLEMENTATION-PLAN.md Phase 1.

Existing-code defects fixed (EX-nn):
- EX-02: reject Content-Length + Transfer-Encoding together (RFC 9112 6.1
  CL.TE/TE.CL smuggling), and conflicting duplicate Content-Length values.
- EX-03: strict, overflow-safe Content-Length parsing, replacing a parser
  that silently skipped non-digit bytes ("5abc" -> 5, "-1" -> 1).
- EX-07: header-read / idle-keep-alive / body-read timeouts enforced by an
  absolute deadline (dev.relism.flash.transport.BufferedByteSource), not
  merely Socket#setSoTimeout, which never trips against a peer trickling
  one byte per read within the window.
- EX-08: header count / name length / value length / request-line length
  bounds (Http1Limits), 431 on violation.
- EX-10: ChunkedInputStream now reads through BufferedByteSource instead
  of the raw unbuffered socket stream, and the header-parser's read-ahead
  bytes are handed over via a zero-copy prependOnce() instead of a
  SequenceInputStream/ByteArrayInputStream pair.
- EX-17: HttpStatus's status-code bound is computed from values() instead
  of a hand-maintained constant that silently threw
  ArrayIndexOutOfBoundsException when a code above it was added; added
  421, 431, 505, 507, 511 and others HTTP/2 and this hardening need.
- EX-18: bare-CR desync and obsolete line folding rejected.
- EX-30: the TLS handshake is forced explicitly, under a timeout, before
  any protocol decision -- SSLSocket#getApplicationProtocol() returned
  null until the handshake had run, and nothing previously forced it.
- EX-31: TLS 1.2 cipher suites on the RFC 9113 Appendix A blocklist are
  filtered out of a listener's enabled set whenever it offers h2 via ALPN.
- EX-35 (found in this phase): Transfer-Encoding values listing multiple
  codings ("gzip, chunked") were silently treated as not chunked at all,
  corrupting the message boundary -- only the whole value was compared.
- EX-36 (found in this phase): a header line with no ':' was silently
  skipped instead of rejected.

New:
- dev.relism.flash.transport.BufferedByteSource: the single buffered,
  deadline-aware, peekable view over a connection's inbound bytes.
- dev.relism.flash.transport.ProtocolNegotiator/NegotiatedProtocol: ALPN
  and h2c prior-knowledge detection. In this phase an H2 result is always
  closed cleanly -- there is no Http2Connection to hand off to until
  Phase 8. FlashConfiguration.http2Enabled gates the h2c preface peek.
- dev.relism.flash.exceptions.MalformedRequestException: a typed,
  status-carrying rejection distinct from HttpException, caught at the
  parse site so a malformed request never reaches the handler chain or
  the user's exception handler, and the connection is always closed.

Two small plan-document corrections recorded as DEC-12 (Phase 1's Files
list omitted BufferedByteSource.java and MalformedRequestException.java;
the request-line-length check description pointed at the wrong offset).
DEC-13/DEC-14 record the deadline and exception-hierarchy designs.

277/277 tests green (flash module), run twice for stability of the new
wall-clock-based HttpServerTimeoutTest cases. Whole-repo build green.
h1 benchmark regression check is left unverified in the plan's DoD: no
JMH harness exists yet (Phase 3 deliverable).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 11:40:03 +00:00

5.1 KiB

HTTP/1.1 Hardening (Phase 1)

Audience: operators. This is the document to read when a 400/413/414/431/501 shows up in the logs and it isn't obvious why. Every rejection rule Flash's HTTP/1.1 parser enforces is listed here with its RFC citation and the status it produces. Contributor-level detail (why each check is implemented the way it is, the exact code paths) lives in the Javadoc of RequestParser, ChunkedInputStream, and dev.relism.flash.exceptions.MalformedRequestException.

Every rejection in this document has one thing in common: the connection is always closed afterwards, never kept alive. A rejected request is exactly the situation a smuggling attack needs a reusable connection for, so none of these rejections offer one — see MalformedRequestException's Javadoc.

Request-smuggling defenses (RFC 9112 §6.1)

Rule Status Detail
Content-Length and Transfer-Encoding both present 400 The canonical CL.TE/TE.CL smuggling vector. Rejected regardless of which header appears first.
Multiple Content-Length lines with differing values 400 Identical repeated values are tolerated (RFC 9110 §8.6 permits treating them as one).
Transfer-Encoding whose final coding is not chunked 501 Flash implements only chunked; anything else (gzip alone, or chunked, gzip — chunked must be last) is unsupported.

Strict Content-Length parsing (RFC 9110 §8.6)

Input Status
Empty value 400
Any non-digit byte (including a leading + or -) 400
More than 19 digits 400
Value overflows Long.MAX_VALUE 400
Value exceeds Http1Limits.MAX_CONTENT_LENGTH (4 GiB by default) 413

The previous parser silently skipped non-digit characters ("5abc" parsed as 5; "-1" parsed as 1) instead of rejecting them — this is the fix.

Header and request-line limits (Http1Limits)

Limit Default Status when exceeded
MAX_HEADER_COUNT 100 431 Request Header Fields Too Large
MAX_HEADER_NAME_LENGTH 256 B 431
MAX_HEADER_VALUE_LENGTH 8192 B 431
MAX_REQUEST_LINE_LENGTH 8192 B 431
Header block exceeds maxHeaderBufferSize (or the connection ends before it completes) configurable, default 64 KiB 431

Line-terminator and header-syntax correctness (RFC 9112 §5)

Rule Status
A \r not immediately followed by \n (bare CR) 400 — a known desynchronization/smuggling surface
A header line beginning with whitespace (obsolete line folding, RFC 9112 §5.2) 400
A header name containing a byte outside RFC 9110 §5.6.2's tchar set 400
A header line with no : 400

Chunked transfer safety (RFC 9112 §7.1, Http1Limits)

Limit Default Status when exceeded
MAX_CHUNK_SIZE 16 MiB 413
Chunk-size line longer than 16 hex digits 400
MAX_CHUNK_EXT_LENGTH (the optional ;name=value after a chunk size) 256 B 400
MAX_CHUNKS_PER_BODY 100 000 413
MAX_TRAILER_COUNT 50 431
A chunk's data not followed by \r\n, or a malformed chunk-size/trailer terminator 400

Trailers are consumed (safely, within the bounds above) but discarded, not exposed to the handler, on HTTP/1.1 today — exposing them via Request.trailers() on both protocols is Phase 12 scope.

Timeouts (FlashConfiguration)

Setting Default Covers
idleKeepAliveTimeoutMs 60 000 How long a keep-alive connection may sit idle waiting for its next request.
headerReadTimeoutMs 10 000 Once the first byte of a request arrives, how long the full header block may take.
bodyReadTimeoutMs 30 000 How long reading the body (by the handler, or the automatic post-response drain) may take.
shutdownDrainTimeoutMs 15 000 How long graceful shutdown waits for in-flight requests before force-closing (wired up starting Phase 2).

These are enforced by an absolute deadline, not merely Socket.setSoTimeout. A per-read socket timeout alone never trips against a peer that sends one byte just often enough to keep each individual read alive (the classic slowloris shape) — see dev.relism.flash.transport.BufferedByteSource's Javadoc for how the absolute deadline is implemented on top of the JDK's per-read-only timeout API.

TLS (RFC 9113 §9.2.2, applies once a listener offers h2 over ALPN)

  • The TLS handshake is forced explicitly (not left to the JDK's lazy on-first-read trigger) before any protocol decision is made, and is bounded by headerReadTimeoutMs.
  • When a listener's TlsConfig.applicationProtocols includes "h2", the enabled TLS 1.2 cipher suite list is filtered against the RFC 9113 Appendix A blocklist (TlsConfig.TLS12_H2_BLOCKED_CIPHERS, ~280 entries). TLS 1.3 is never affected — none of its cipher suites are on that list.
  • HTTP/2 itself is not yet served in this phase (lands in Phase 8): a connection that negotiates h2 via ALPN, or that opens with the h2c prior-knowledge preface while FlashConfiguration.http2Enabled is set, is currently closed cleanly rather than served.