Conversation
receive_headers handled every HEADERS block, trailers included, and called _initialize_content_length on all of them. _track_content_length was only ever called from receive_data, so a stream ended by a trailers section never reached the "end_stream and expected != actual" branch: - trailers without content-length left the expectation as None and the guard was skipped entirely; - trailers with content-length silently replaced the header-section value, so a peer could declare 10 in the headers and 3 in the trailers and have a 13-byte body accepted. RFC 9113 section 8.1.1 makes a message malformed when content-length does not equal the sum of the DATA payload lengths, and the exemptions it lists are 204, 304 and HEAD, not trailers. The too-much-data direction still errored, because it trips while receiving DATA, so only the short-body direction was silently accepted. Run the same parse on trailers so an invalid content-length there is still a ProtocolError, then put the previous expectation back, and validate the body where the stream actually ends.
|
I am not sure if I understand the problem stated here. To my understanding, a If a HEADERS frame with END_STREAM set is received after DATA, the content length is already known or final, so there is no need to parse or validate it again. So the only missing call is possibly |
|
Thanks for the review. The length check now runs before |
| c.clear_outbound_data_buffer() | ||
|
|
||
| trailers = frame_factory.build_headers_frame( | ||
| headers=[("content-length", "banana")], |
There was a problem hiding this comment.
Trailers are not allowed to have content-length headers at all - no matter their value.
There was a problem hiding this comment.
Same rule as the other thread, detail there: 28cc3a5 rejects a content-length received in a trailers section whatever its value, via _reject_content_length_in_trailers in utilities.validate_headers, with the test parametrized over 13, 15, 0 and banana.
| c.clear_outbound_data_buffer() | ||
|
|
||
| trailers = frame_factory.build_headers_frame( | ||
| headers=[("content-length", "13"), ("x-checksum", "0")], |
There was a problem hiding this comment.
Trailers are not allowed to have content-length headers at all - no matter their value.
There was a problem hiding this comment.
Done in 28cc3a5, and my earlier "base already refuses it" was wrong for numeric values — correction posted in the conversation.
_reject_content_length_in_trailers now sits in the validate_headers pipeline next to _reject_connection_header, keys off the same hdr_validation_flags.is_trailer flag as the pseudo-header-in-trailer rule, and raises ProtocolError("Received content-length header in trailer") whatever the value:
for header in headers:
if hdr_validation_flags.is_trailer and header[0] == b"content-length":
msg = "Received content-length header in trailer"
raise ProtocolError(msg)
yield headertest_content_length_rejected_in_trailers is parametrized over 13, 15, 0, banana, each also asserting the PROTOCOL_ERROR GOAWAY. stream.py stays at the one-line change you asked for.
One question rather than an assumption: the send path is untouched, so send_headers still emits a trailer content-length if a caller passes one, and four existing tests in test_basic_logic.py do exactly that with content-length: 0. Do you want the outbound side symmetric, or was the receive-side rule the one you meant?
|
@Kriechi good catch — trailers must not carry That made the receive-side trailer fixtures in Still the same fix underneath: a body that ends with trailers is policed where the stream actually ends. |
|
Both of these are addressed in
The rest of the change is separate from that rule and is what the earlier commits do: a trailers section that ends a stream without a If the RFC 9110 § 6.5.1 reading is not what you want for the mismatch cases — i.e. if you would rather a |
Base already refuses a content-length in a trailers section: it runs the field through the regular content-length parser, which rejects it (and rejects a non-numeric value as 'Invalid content-length header'). The explicit check added a second, differently-worded rejection for the same input, so it was redundant. What base does not do is police the body length when a stream ends with trailers, which is what Kriechi identified. Keep just that call.
|
You were right on both counts, and I checked rather than argued. Narrowed to your one-liner. On On HEADERS-with-END_STREAM after DATA — agreed. Once DATA has arrived the body length is already known, and a trailers block is not the place to re-initialise it. That is now the only structural change: if isinstance(headers_event, TrailersReceived):
# Trailers are not part of the content, but the stream ends here,
# so this is the only point at which the body length can be policed.
self._track_content_length(0, end_stream=True)
else:
self._initialize_content_length(headers)
So Full suite on the narrowed branch: One thing I did not do, so it is not silently assumed: I did not add a |
|
Correcting something I published here, because it changed what this branch did. In an earlier comment I wrote that base already refuses a
|
Fixes #1328
Description
A stream that ends with a trailers section never had its
content-lengthpoliced, and acontent-lengthin the trailers was accepted as if it were the one from the header section.H2Stream.receive_headershandles every received HEADERS block, trailers included, and called_initialize_content_length(headers)unconditionally._track_content_length, where the comparison actually happens, was only ever called fromreceive_data, soend_streamwas neverTruefor a body that ends with trailers. A peer declaringcontent-length: 15could send 13 bytes and then a trailers section, and the short body was accepted.Per @Kriechi's review, a
content-lengthreceived in a trailers section is now rejected whatever its value, in the samevalidate_headerspipeline that already rejects a pseudo-header there: RFC 9110 § 6.5.1 keeps fields that describe message framing out of trailer sections, because they have to be evaluated before the content is received. Trailers that end a stream run the same length check as any other end of stream, so they can neither reset nor redefine the expectation.Test plan
h2 4.4.1 installed editable from this branch, Python 3.11.15 on macOS arm64.
masteris measured from its own source atbc239af1d1b85bc70482804f30a0e0e587d90a08, under the same interpreter and the samehpack/hyperframe, so only h2's source differs. Unit level with the existingframe_factoryfixture, in-memory bytes, no network.TestContentLengthEnforcedAtTrailersintests/test_invalid_content_lengths.py: insufficient data ended by trailers, no data at all ended by trailers,content-lengthin trailers rejected for13,15,0andbanana, a matching body ended by trailers still accepted withTrailersReceived, and a request with nocontent-lengthunaffected. Four receive-side tests intests/test_basic_logic.pyusedcontent-length: 0as their trailer field and now usex-checksum.Command output
This branch's tests run against
master's source (PYTHONPATH=<master src>), i.e. before the fix:The two that pass either way pin that valid trailers and a stream with no
content-lengthare still accepted. Note thebananacase:masterdoes refuse a non-numeric trailercontent-length, through the ordinary parser and with different wording, so it fails on the message rather than on "did not raise". The numeric values are accepted onmasterand redefine_expected_content_length.After, on this branch:
Unwiring the new check from
validate_headers, with the tests left in place, fails exactly the fourtest_content_length_rejected_in_trailers[...]cases and nothing else.Not run: the
h2spectox env (needs a downloaded Go binary and a live TLS server) and thepackaging/docsenvs; and no CI has executed on this PR, since the check suite sits ataction_requiredfor fork branches. Everything above is local.