mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-27 12:31:28 +00:00
Reject an invalid Content-Length and honor Connection: close
A request whose Content-Length was present but not a valid decimal length (e.g. "42, 42", "+42", "0x2e" or empty) was treated as having no body unless a handler read it: no 400 was returned, the body was not drained, and the bytes after the header block were parsed as the next request on the keep-alive connection. Reject such a request with 400 and close the connection before routing, as RFC 9112 Section 6.3 requires. The server also kept reading after a response that announced Connection: close. A rejected request line or header block left the rest of the message to be parsed as a new request, and an error response to a bodyless request did the same with whatever followed. Close the connection whenever the final response carries Connection: close (RFC 9112 Section 9.6), and mark the two request-head rejection paths closed explicitly as the other rejection paths already do.
This commit is contained in:
1 parent
0715c2739e
commit
639391ad7f
2 files changed
+152
-51
No files matched your search
@@ -14380,8 +14380,21 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
res.version = "HTTP/1.1";
|
||||
res.headers = default_headers_;
|
||||
|
||||
// Request line and headers
|
||||
// RFC 9112 §9.6: a server that sends the "close" connection option must
|
||||
// close the connection after that response, whichever path wrote it (an
|
||||
// error status, a handler, or a rejected request). Reading on would also
|
||||
// parse whatever the client sent next on a connection it considers done.
|
||||
auto honor_connection_close = detail::scope_exit([&] {
|
||||
if (detail::has_header_token(res.headers, "Connection", "close")) {
|
||||
connection_closed = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Request line and headers. A rejected message leaves the rest of it (and
|
||||
// any body) unread, so the connection cannot be reused: the leftover bytes
|
||||
// would be parsed as the next request.
|
||||
if (!parse_request_line(line_reader.ptr(), req)) {
|
||||
connection_closed = true;
|
||||
res.status = StatusCode::BadRequest_400;
|
||||
output_error_log(Error::InvalidRequestLine, &req);
|
||||
return write_response(strm, close_connection, req, res);
|
||||
@@ -14389,20 +14402,28 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
|
||||
// Request headers
|
||||
if (!detail::read_headers(strm, req.headers)) {
|
||||
connection_closed = true;
|
||||
res.status = StatusCode::BadRequest_400;
|
||||
output_error_log(Error::InvalidHeaders, &req);
|
||||
return write_response(strm, close_connection, req, res);
|
||||
}
|
||||
|
||||
// RFC 9112 §6.3: Reject requests whose framing is ambiguous, which would
|
||||
// otherwise let an intermediary and this parser disagree on where the body
|
||||
// ends and enable request smuggling. Two cases: a non-zero Content-Length
|
||||
// alongside any Transfer-Encoding (Content-Length: 0 is tolerated for
|
||||
// compatibility with existing clients), and a Transfer-Encoding whose final
|
||||
// coding is not chunked, which leaves the body length undeterminable. The
|
||||
// latter must not fall through to the "no body" path, or the body bytes are
|
||||
// parsed as the next request on a persistent connection.
|
||||
if (detail::has_conflicting_content_length(req.headers) ||
|
||||
// RFC 9112 §6.3: Reject requests whose framing is invalid or ambiguous,
|
||||
// which would otherwise let an intermediary and this parser disagree on
|
||||
// where the body ends and enable request smuggling. Three cases: a
|
||||
// Content-Length that is not a valid decimal length (e.g. "42, 42", "+42"
|
||||
// or empty), which would otherwise be read as "no body"; a non-zero
|
||||
// Content-Length alongside any Transfer-Encoding (Content-Length: 0 is
|
||||
// tolerated for compatibility with existing clients); and a
|
||||
// Transfer-Encoding whose final coding is not chunked, which leaves the body
|
||||
// length undeterminable. None of them may fall through to the "no body"
|
||||
// path, or the body bytes are parsed as the next request on a persistent
|
||||
// connection.
|
||||
auto is_invalid_content_length = false;
|
||||
detail::get_header_value_u64(req.headers, "Content-Length", 0, 0,
|
||||
is_invalid_content_length);
|
||||
if (is_invalid_content_length ||
|
||||
detail::has_conflicting_content_length(req.headers) ||
|
||||
(req.has_header("Transfer-Encoding") &&
|
||||
!detail::is_chunked_transfer_encoding(req.headers))) {
|
||||
connection_closed = true;
|
||||
@@ -14675,17 +14696,13 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
// keep-alive. Without framing there is no body to drain — reading would
|
||||
// consume the next request (issue #2450). If the response has committed the
|
||||
// connection to close, there is no next request to protect.
|
||||
if (!req.body_consumed_ && detail::has_framed_body(req)) {
|
||||
if (detail::has_header_token(res.headers, "Connection", "close")) {
|
||||
if (!req.body_consumed_ && detail::has_framed_body(req) &&
|
||||
!detail::has_header_token(res.headers, "Connection", "close")) {
|
||||
int dummy_status;
|
||||
if (!detail::read_content(
|
||||
strm, req, payload_max_length_, dummy_status, nullptr,
|
||||
[](const char *, size_t, size_t, size_t) { return true; }, false)) {
|
||||
connection_closed = true;
|
||||
} else {
|
||||
int dummy_status;
|
||||
if (!detail::read_content(
|
||||
strm, req, payload_max_length_, dummy_status, nullptr,
|
||||
[](const char *, size_t, size_t, size_t) { return true; },
|
||||
false)) {
|
||||
connection_closed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user