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:
yhirose committed 2026-10-02 00:22:24 -04:00
1 parent 0715c2739e
commit 639391ad7f
2 files changed
+152 -51

No files matched your search

+37 -20
View File
@@ -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;
}
}
}