streams: reject connection-specific header fields in send_trailers (RFC 9113 §8.2.2) - #925
Merged
seanmonstar merged 3 commits intoJul 28, 2026
Conversation
…FC 9113 §8.2.2) Trailers are carried in a HEADERS frame and are subject to the same prohibition on connection-specific header fields (RFC 9113 §8.2.2) as every other outbound HEADERS block. `send_headers`, `send_push_promise` and `send_interim_informational_headers` all reject these via `check_headers`, and the receive path (`load_hpack`) treats such a header as malformed — but `send_trailers` did not, making it the only send path that would *generate* a message §8.2.2 forbids. Apply the same `check_headers` before the state transition, so a rejected call leaves the stream able to send valid trailers. Adds a regression test asserting that connection/keep-alive/proxy-connection/ transfer-encoding/upgrade and a non-"trailers" TE are each rejected in a trailer block, and that a subsequent valid trailer still sends.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
streams: reject connection-specific header fields in
send_trailers(RFC 9113 §8.2.2)Summary
RFC 9113 §8.2.2 forbids connection-specific header fields (
Connection,Keep-Alive,Proxy-Connection,Transfer-Encoding,Upgrade, andTEother thanTE: trailers) in HTTP/2, inboth directions:
h2already enforces both halves of that — except on one send path. Trailers are carried in aHEADERS frame, so they are subject to the same rule, but
Send::send_trailersis the only outboundHEADERS path that does not validate its fields:
send_headers(send.rs)check_headerssend_push_promisecheck_headerssend_interim_informational_headerscheck_headerssend_trailersframe/headers.rsload_hpack)As a result, a caller that puts e.g.
transfer-encodinginto a trailerHeaderMapmakesh2generate and transmit a TRAILERS frame carrying a header the same crate (a) rejects with
UserError::MalformedHeaderson every other send call and (b) treats as a malformed message,PROTOCOL_ERROR, on receive. The publicSendStream::send_trailersdocs do not state that the calleris responsible for this, so the inconsistency is silent.
Fix
Call the existing
check_headersfromsend_trailers, before the state transition, so a rejected callreturns
UserError::MalformedHeadersand leaves the stream able to send valid trailers — exactly thepolicy the other three send paths and the receive path already apply. No new policy, just consistency.
check_headersalready encodes theTE: trailersexception, so valid trailers (includingTE: trailers) are unaffected. This mirrors the receive path byte-for-byte.Why it matters
This is primarily a conformance / internal-consistency fix:
h2should not generate a message itdefines as malformed on receipt. It is not remotely exploitable against a conformant peer — a compliant
HTTP/2 receiver (including
h2itself) resets the stream withPROTOCOL_ERROR, so the exchange failsclosed. The relevance is for deployments where
h2output reaches a lenient or downgrading downstream(e.g. an HTTP/2→HTTP/1.1 gateway that serializes trailers without re-filtering), where a
connection-specific header smuggled in a trailer is a known request-smuggling primitive. Emitting it at
all is the part this crate controls, and §8.2.2 says not to.
The gap is reachable in practice through
hyper: as a reverse proxy,hyperforwards an upstreamresponse/request body's trailers verbatim (its
strip_connection_headersis applied only to the mainhead, not to the trailer block), so a trailer set by a malicious or misbehaving upstream is handed
straight to
send_trailers. This was demonstrated end to end — a malicious HTTP/1.1 backend'stransfer-encodingtrailer, forwarded through ahyperHTTP/2 reverse proxy, is re-emitted on theHTTP/2 wire and rejected by the compliant client with
PROTOCOL_ERROR. (Notably,hyper's own HTTP/1trailer encoder already filters these fields via its
is_valid_trailer_fieldallow-list; the HTTP/2egress path relies on
h2, where the check is currently absent — so this change also brings the twoegress directions into line.)
How this was verified
hyperHTTP/2 client sending a bodywhose trailers carry
transfer-encoding: chunked. Frame tracing showsh2writing the trailerHEADERS frame onto the wire (
send_trailers -- queuing→framed_write: send frame=Headers{… END_STREAM}); a complianth2server rejects it (load_hpack; connection level header→malformed message→stream error PROTOCOL_ERROR). A benign trailer on the same path is accepted — a cleandifferential.
transfer-encodingtrailer, forwarded through a
hyperh2-server /h1-client reverse proxy, is re-emitted on theHTTP/2 wire (the h2 client resets with
PROTOCOL_ERROR), while a benign trailer forwards cleanly. Thesame backend behind an
h1-server /h1-clienthyperproxy has itstransfer-encodingtrailerfiltered out — confirming the HTTP/1 encoder already enforces this and only the HTTP/2 path does not.
send_trailers_rejects_connection_specific_headers) fails onmaster(the poisoned trailer is accepted and the stream is cancelled instead of completing) and passes with
this change. The full
h2-testssuite (202 passing tests across 12 files) stays green.Test
tests/h2-tests/tests/trailers.rs::send_trailers_rejects_connection_specific_headersasserts thatconnection,keep-alive,proxy-connection,transfer-encoding,upgrade, andte: <not trailers>are each rejected in a trailer block with
user error: malformed headers, and that a subsequent validtrailer still sends and the exchange completes — confirming the rejection does not corrupt stream state.
Found and verified with rust-in-peace, an
AI-agent-assisted Rust security-review pipeline; the diff and test were authored and run against the
crate at current
master.