Repository navigation
Validate trailer fields in write_chunked_body regardless of the collection type. - #67
Open
samuel-williams-shopify wants to merge 8 commits into
Open
samuel-williams-shopify wants to merge 8 commits into
samuel-williams-shopify wants to merge 8 commits into
Conversation
…patible encodings between fields.
…irs rather than a `Protocol::HTTP::Headers` instance.
samuel-williams-shopify
force-pushed
the
validate-written-trailers
branch
from
November 5, 2026 10:02
f2d8d7c to
9b94484
Compare
This branch has not been deployed
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.
write_chunked_bodynow validates every trailer field againstProtocol::HTTP::Headers::POLICYbefore writing the trailer section, so framing fields such ascontent-lengthandtransfer-encodingraiseProtocol::HTTP::InvalidTrailerErrorinstead of being written to the wire. Additionally, a connection whose body write fails part-way is now marked non-persistent so it can never be reused.Problem
write_chunked_body(body, head, trailer)(reached viawrite_body) wrote whatever thetrailerargument yielded after the terminal0\r\nchunk. Only fields added throughProtocol::HTTP::Headers#addwithtrailer: truewere checked against the trailer policy; a plainHashor any other enumerable bypassed that check entirely, so a caller could emit:Framing fields in a trailer are forbidden by RFC 9110 §6.5.1 and are a request/response smuggling vector when an intermediary honours them.
Separately, when
write_chunked_bodyorwrite_fixed_length_bodyraised mid-body (e.g.ContentLengthError, or nowInvalidTrailerError), theensurebranch still calledsend_end_stream!, which on a server in the half-closed remote state callsclose!with no error and returns a still-persistent connection to the:idlestate. A caller which swallowed the error could therefore reuse a connection with a half-written body on it.Solution
Connection#format_trailer(trailer), which iterates the trailer, looks up each name (case-insensitively) inProtocol::HTTP::Headers::POLICY, and raisesProtocol::HTTP::InvalidTrailerErrorwhen the policy isfalse(single-valued headers such ascontent-length) or the header class reportstrailer?asfalse(e.g.transfer-encoding,trailer). Unlisted names fall back toHeader::Generic, which permits trailers, matching the behaviour ofHeaders#add. The same error class is already raised by the read side (Body::Chunked#read_trailer), so callers see consistent errors in both directions.0\r\nis written, so an invalid trailer leaves the chunked body visibly unterminated rather than half-emitted with a forbidden field. Each formatted line is coerced with.bbefore being appended, since appending to an emptyStringadopts the argument's encoding and a UTF-8 value followed by a binary value would otherwise raiseEncoding::CompatibilityError. Token/CR/LF/NUL validation is reused by extractingformat_headerfromwrite_header(which is otherwise unchanged).downcaseper field, and only when a trailer is actually present.write_chunked_bodyandwrite_fixed_length_bodynow set@persistent = falsein their existingrescuebranches, sosend_end_stream!transitions to:closedrather than:idleafter a failed body write. (write_body_and_closealready does this unconditionally.)trailerparameter is documented asHash | Enumerable | Nilof[name, value]pairs, matching what is accepted, e.g. the enumerator returned byProtocol::HTTP::Headers#trailer!.Tests
test/protocol/http1/connection.rb: Hash trailers containingcontent-length/transfer-encoding(including mixed case) and an array-of-pairs trailer raiseInvalidTrailerError, and the exact captured wire output confirms neither the forbidden field nor the terminal chunk was written. A trailer produced byHeaders#trailer!(as async-http does) continues to round-trip, including when the trailer field is added to the headers while the body is still being written. Mixed UTF-8 and binary trailer values are written byte-for-byte. After a rejected trailer or aContentLengthError, the connection is non-persistent and does not return to:idle.test/protocol/http1/trailer.rb: the same rejection throughwrite_body, asserting the exact bytes written.