@@ -4723,17 +4723,24 @@ write_multipart_ranges_data(Stream &strm, const Request &req, Response &res,
47234723 });
47244724}
47254725
4726+ bool has_framed_body(const Request &req) {
4727+ return is_chunked_transfer_encoding(req.headers) ||
4728+ req.get_header_value_u64("Content-Length") > 0;
4729+ }
4730+
4731+ bool is_connection_persistent(const Request &req) {
4732+ auto conn = req.get_header_value("Connection");
4733+ if (conn == "close") { return false; }
4734+ if (req.version == "HTTP/1.0" && conn != "Keep-Alive") { return false; }
4735+ return true;
4736+ }
4737+
47264738bool expect_content(const Request &req) {
47274739 if (req.method == "POST" || req.method == "PUT" || req.method == "PATCH" ||
47284740 req.method == "DELETE") {
47294741 return true;
47304742 }
4731- if (req.has_header("Content-Length") &&
4732- req.get_header_value_u64("Content-Length") > 0) {
4733- return true;
4734- }
4735- if (is_chunked_transfer_encoding(req.headers)) { return true; }
4736- return false;
4743+ return has_framed_body(req);
47374744}
47384745
47394746#ifdef _WIN32
@@ -7449,29 +7456,18 @@ bool Server::read_content_core(
74497456 size_t /*len*/) { return receiver(buf, n); };
74507457 }
74517458
7452- // RFC 7230 Section 3.3.3: If this is a request message and none of the above
7453- // are true (no Transfer-Encoding and no Content-Length), then the message
7454- // body length is zero (no message body is present).
7455- //
7456- // For non-SSL builds, detect clients that send a body without a
7457- // Content-Length header (raw HTTP over TCP). Check both the stream's
7458- // internal read buffer (data already read from the socket during header
7459- // parsing) and the socket itself for pending data. If data is found and
7460- // exceeds the configured payload limit, reject with 413.
7461- // For SSL builds we cannot reliably peek the decrypted application bytes,
7462- // so keep the original behaviour.
7459+ // RFC 9112 §6: no Transfer-Encoding and no Content-Length means no body.
7460+ // For non-SSL builds we still scan non-persistent connections for stray
7461+ // body bytes so the payload limit is enforced (413). On keep-alive,
7462+ // pending bytes may be the next request (issue #2450), so skip.
74637463#if !defined(CPPHTTPLIB_SSL_ENABLED)
74647464 if (!req.has_header("Content-Length") &&
74657465 !detail::is_chunked_transfer_encoding(req.headers)) {
7466- // Only check if payload_max_length is set to a finite value
7467- if (payload_max_length_ > 0 &&
7466+ if (!detail::is_connection_persistent(req) && payload_max_length_ > 0 &&
74687467 payload_max_length_ < (std::numeric_limits<size_t>::max)()) {
7469- // Check if there is data already buffered in the stream (read during
7470- // header parsing) or pending on the socket. Use a non-blocking socket
7471- // check to avoid deadlock when the client sends no body.
7472- bool has_data = strm.is_readable();
7468+ auto has_data = strm.is_readable();
74737469 if (!has_data) {
7474- socket_t s = strm.socket();
7470+ auto s = strm.socket();
74757471 if (s != INVALID_SOCKET) {
74767472 has_data = detail::select_read(s, 0, 0) > 0;
74777473 }
@@ -8033,6 +8029,11 @@ get_client_ip(const std::string &x_forwarded_for,
80338029 ip_list.emplace_back(std::string(b + r.first, b + r.second));
80348030 });
80358031
8032+ // A malformed X-Forwarded-For (empty, comma-only, whitespace-only) yields
8033+ // no segments. Signal "no client IP derived" with an empty string so the
8034+ // caller can fall back to the connection-level remote address.
8035+ if (ip_list.empty()) { return std::string(); }
8036+
80368037 for (size_t i = 0; i < ip_list.size(); ++i) {
80378038 auto ip = ip_list[i];
80388039
@@ -8123,7 +8124,8 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
81238124
81248125 if (!trusted_proxies_.empty() && req.has_header("X-Forwarded-For")) {
81258126 auto x_forwarded_for = req.get_header_value("X-Forwarded-For");
8126- req.remote_addr = get_client_ip(x_forwarded_for, trusted_proxies_);
8127+ auto derived = get_client_ip(x_forwarded_for, trusted_proxies_);
8128+ req.remote_addr = derived.empty() ? remote_addr : derived;
81278129 } else {
81288130 req.remote_addr = remote_addr;
81298131 }
@@ -8325,15 +8327,14 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
83258327 ret = write_response(strm, close_connection, req, res);
83268328 }
83278329
8328- // Drain any unconsumed request body to prevent request smuggling on
8329- // keep-alive connections.
8330- if (!req.body_consumed_ && detail::expect_content(req)) {
8331- int drain_status = 200; // required by read_content signature
8330+ // Drain any unconsumed framed body to prevent request smuggling on
8331+ // keep-alive. Without framing there is no body to drain — reading would
8332+ // consume the next request (issue #2450).
8333+ if (!req.body_consumed_ && detail::has_framed_body(req)) {
8334+ int dummy_status;
83328335 if (!detail::read_content(
8333- strm, req, payload_max_length_, drain_status , nullptr,
8336+ strm, req, payload_max_length_, dummy_status , nullptr,
83348337 [](const char *, size_t, size_t, size_t) { return true; }, false)) {
8335- // Body exceeds payload limit or read error — close the connection
8336- // to prevent leftover bytes from being misinterpreted.
83378338 connection_closed = true;
83388339 }
83398340 }
0 commit comments