http2/stream: fix trailers validation#237
http2/stream: fix trailers validation#237Ph4ntomas wants to merge 2 commits intodaurnimator:masterfrom
Conversation
Signed-off-by: phantomas <phantomas@phantomas.xyz>
problem: h2_stream validate_header reject valid response trailers, while accepting invalid ones. cause: validate_header checks that all headers contains the ':status' pseudo-header, which is disallowed by the RFC. fix: add an 'expect_trailers' parameter to validate_header. This value is set to true after a final (i.e. non 1XX) header has validated. When this flag is set, pseudo-headers are disallowed, and ended_stream must be set on the next header validation. Signed-off-by: phantomas <phantomas@phantomas.xyz>
6d333e1 to
e879651
Compare
|
@daurnimator this should address your comment in #236 |
| end | ||
| if stream.type == "client" and not stream.recv_final_header then | ||
| local status = headers:get(":status") | ||
| stream.recv_final_header = string.match(status, "1%d%d") == nil |
There was a problem hiding this comment.
I don't think this is the correct way to check this.
There was a problem hiding this comment.
I disagree, but let me explain why I've implemented the fix like this (quote are from RFC 9113 8.1, unless otherwise specified). If you don´t agree after that, I´ll go with the fix in the next change request.
The standard describe a message as:
An HTTP message (request or response) consists of:
- one HEADERS frame (followed by zero or more CONTINUATION frames) containing the header section (see Section 6.3 of [HTTP]),
- zero or more DATA frames containing the message content (see Section 6.4 of [HTTP]), and
- optionally, one HEADERS frame (followed by zero or more CONTINUATION frames) containing the trailer section, if present (see Section 6.5 of [HTTP]).
Since 2. is optional, we can't rely on the presence of data to know the next header frame will be a trailer (the message could be bunch of interim response, followed by a single final header, followed by a trailer).
We can't rely on the number of headers either because of interim responses.
An interim response consists of a HEADERS frame (which might be followed by zero or more CONTINUATION frames) containing the control data and header section of an interim (1xx) HTTP response
From RFC 9110:
A 1xx response is terminated by the end of the header section; it cannot contain content or trailers.
This means that regardless of the amount of interim responses, there will be at most one trailer, which will be held in a header field with the END_STREAM flag set.
For a response only, a server MAY send any number of interim responses before the HEADERS frame containing a final response.
[...]
An endpoint that receives a HEADERS frame without the END_STREAM flag set after receiving the HEADERS frame that opens a request or after receiving a final (non-informational) status code MUST treat the corresponding request or response as malformed (Section 8.1.1).
Since only interim responses are allowed before the "final" response, once we've seen the final response (i.e. a non 1xx status code), we know that the next header frame (if any) must be a trailer.
From all of this, my understanding is that only trailers can appear after a non-informational status code was receive. Since all responses (interim or final) must include a ":status" pseudo header, and everything else is optional, this code is the only thing that can reliably identify the next HEADER as a trailer on a response.
There was a problem hiding this comment.
An endpoint that receives a HEADERS frame without the END_STREAM flag set after receiving the HEADERS frame that opens a request or after receiving a final (non-informational) status code MUST treat the corresponding request or response as malformed (Section 8.1.1).
Ah, that's a better rule to quote+live by!
So not string.find(status, "^1%d%d$") is indeed a reasonable check.
However that's only a stream level error.
vs the nth_header == 1 change I suggested should still be a protocol error I think? Or at least I vaguely remember clients/a test suite expecting it to be a protocol error
There was a problem hiding this comment.
However that's only a stream level error.
vs the nth_header == 1 change I suggested should still be a protocol error I think?
I'm not sure I follow. This just mark the stream so we know we're in the last message, but doesn't trigger any error by itself. The mark is then forwarded to validate_headers as its expect_trailer parameter.
A protocol error is warranted (and returned) we then see a header:
| return nil, h2_errors.PROTOCOL_ERROR:new_traceback("An HTTP request consists of maximum 2 HEADER blocks", true), ce.EILSEQ | ||
| end | ||
| else | ||
| elseif not expect_trailer then |
There was a problem hiding this comment.
I think a better valid fix to this whole issue might be just changing this line to:
| elseif not expect_trailer then | |
| elseif nth_header == 1 then |
and fixing up the comment to say that not all header blocks (such as trailers) will have a status field
There was a problem hiding this comment.
Doing this will let invalid header go through if at least one interim response was received. If anything, the status check should only be removed when ended_stream is true, as this will limit the amount of false positive.
problem: h2_stream validate_header reject valid trailers
cause: for response headers, validate_header ignores the
nth_headerparameter, and checked the pseudo-header:status, even for response trailers.fix: only check for
:statuson the first header.