-
-
Notifications
You must be signed in to change notification settings - Fork 87
http2/stream: fix trailers validation #237
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -99,6 +99,9 @@ local function new_stream(connection) | |||||||||||||
| recv_headers_fifo = new_fifo(); | ||||||||||||||
| recv_headers_cond = cc.new(); | ||||||||||||||
|
|
||||||||||||||
| recv_final_header = false; | ||||||||||||||
| sent_final_header = false; | ||||||||||||||
|
|
||||||||||||||
| chunk_fifo = new_fifo(); | ||||||||||||||
| chunk_cond = cc.new(); | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -371,7 +374,7 @@ local valid_pseudo_headers = { | |||||||||||||
| [":authority"] = true; | ||||||||||||||
| [":status"] = false; | ||||||||||||||
| } | ||||||||||||||
| local function validate_headers(headers, is_request, nth_header, ended_stream) | ||||||||||||||
| local function validate_headers(headers, is_request, nth_header, ended_stream, expect_trailer) | ||||||||||||||
| -- Section 8.1.2: A request or response containing uppercase header field names MUST be treated as malformed | ||||||||||||||
| for name in headers:each() do | ||||||||||||||
| if name:lower() ~= name then | ||||||||||||||
|
|
@@ -389,7 +392,7 @@ local function validate_headers(headers, is_request, nth_header, ended_stream) | |||||||||||||
| Pseudo-header fields MUST NOT appear in trailers. | ||||||||||||||
| Endpoints MUST treat a request or response that contains | ||||||||||||||
| undefined or invalid pseudo-header fields as malformed]] | ||||||||||||||
| if (is_request and nth_header ~= 1) or valid_pseudo_headers[name] ~= is_request then | ||||||||||||||
| if (is_request and nth_header ~= 1) or valid_pseudo_headers[name] ~= is_request or expect_trailer then | ||||||||||||||
| return nil, h2_errors.PROTOCOL_ERROR:new_traceback("Pseudo-header fields are only valid in the context in which they are defined", true), ce.EILSEQ | ||||||||||||||
| end | ||||||||||||||
| if seen_non_colon then | ||||||||||||||
|
|
@@ -440,14 +443,18 @@ local function validate_headers(headers, is_request, nth_header, ended_stream) | |||||||||||||
| elseif nth_header > 2 then | ||||||||||||||
| 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 | ||||||||||||||
| --[[ For HTTP/2 responses, a single :status pseudo-header field is | ||||||||||||||
| defined that carries the HTTP status code field (RFC7231, Section 6). | ||||||||||||||
| This pseudo-header field MUST be included in all responses; otherwise, | ||||||||||||||
| the response is malformed (Section 8.1.2.6)]] | ||||||||||||||
| if not headers:has(":status") then | ||||||||||||||
| return nil, h2_errors.PROTOCOL_ERROR:new_traceback(":status pseudo-header field MUST be included in all responses", true), ce.EILSEQ | ||||||||||||||
| end | ||||||||||||||
| else | ||||||||||||||
| if not ended_stream then | ||||||||||||||
| return nil, h2_errors.PROTOCOL_ERROR:new_traceback("Trailers MUST be at end of stream", true), ce.EILSEQ | ||||||||||||||
| end | ||||||||||||||
| end | ||||||||||||||
| return true | ||||||||||||||
| end | ||||||||||||||
|
|
@@ -474,13 +481,17 @@ local function process_end_headers(stream, end_stream, pad_len, pos, promised_st | |||||||||||||
|
|
||||||||||||||
| if not promised_stream then | ||||||||||||||
| stream.stats_recv_headers = stream.stats_recv_headers + 1 | ||||||||||||||
| local validate_ok, validate_err, errno2 = validate_headers(headers, stream.type ~= "client", stream.stats_recv_headers, end_stream) | ||||||||||||||
| local validate_ok, validate_err, errno2 = validate_headers(headers, stream.type ~= "client", stream.stats_recv_headers, end_stream, stream.recv_final_header) | ||||||||||||||
| if not validate_ok then | ||||||||||||||
| return nil, validate_err, errno2 | ||||||||||||||
| end | ||||||||||||||
| if headers:has("content-length") then | ||||||||||||||
| stream.content_length = tonumber(headers:get("content-length"), 10) | ||||||||||||||
| 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 | ||||||||||||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this is the correct way to check this.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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:
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.
From RFC 9110:
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.
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.
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ah, that's a better rule to quote+live by! vs the
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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 A protocol error is warranted (and returned) we then see a header:
|
||||||||||||||
| end | ||||||||||||||
| stream.recv_headers_fifo:push(headers) | ||||||||||||||
|
|
||||||||||||||
| if end_stream then | ||||||||||||||
|
|
@@ -498,7 +509,7 @@ local function process_end_headers(stream, end_stream, pad_len, pos, promised_st | |||||||||||||
| end | ||||||||||||||
| end | ||||||||||||||
| else | ||||||||||||||
| local validate_ok, validate_err, errno2 = validate_headers(headers, true, 1, false) | ||||||||||||||
| local validate_ok, validate_err, errno2 = validate_headers(headers, true, 1, false, false) | ||||||||||||||
| if not validate_ok then | ||||||||||||||
| return nil, validate_err, errno2 | ||||||||||||||
| end | ||||||||||||||
|
|
@@ -1321,9 +1332,14 @@ end | |||||||||||||
|
|
||||||||||||||
| function stream_methods:write_headers(headers, end_stream, timeout) | ||||||||||||||
| assert(headers, "missing argument: headers") | ||||||||||||||
| assert(validate_headers(headers, self.type == "client", self.stats_sent_headers+1, end_stream)) | ||||||||||||||
| assert(validate_headers(headers, self.type == "client", self.stats_sent_headers+1, end_stream, self.sent_final_header)) | ||||||||||||||
| assert(type(end_stream) == "boolean", "'end_stream' MUST be a boolean") | ||||||||||||||
|
|
||||||||||||||
| if self.type ~= "client" and not self.sent_final_header then | ||||||||||||||
| local status = headers:get(":status") | ||||||||||||||
| self.sent_final_header = string.match(status, "1%d%d") == nil | ||||||||||||||
| end | ||||||||||||||
|
|
||||||||||||||
| local padded, exclusive, stream_dep, weight = nil, nil, nil, nil | ||||||||||||||
| return write_headers(self, function(payload, end_headers, deadline) | ||||||||||||||
| return self:write_headers_frame(payload, end_stream, end_headers, padded, exclusive, stream_dep, weight, deadline and deadline-monotime()) | ||||||||||||||
|
|
@@ -1333,7 +1349,7 @@ end | |||||||||||||
| function stream_methods:push_promise(headers, timeout) | ||||||||||||||
| assert(self.type == "server") | ||||||||||||||
| assert(headers, "missing argument: headers") | ||||||||||||||
| assert(validate_headers(headers, true, 1, false)) | ||||||||||||||
| assert(validate_headers(headers, true, 1, false, false)) | ||||||||||||||
| assert(headers:has(":authority"), "PUSH_PROMISE must have an :authority") | ||||||||||||||
|
|
||||||||||||||
| local promised_stream = self.connection:new_stream() | ||||||||||||||
|
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a better valid fix to this whole issue might be just changing this line to:
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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_streamis true, as this will limit the amount of false positive.