diff --git a/scripts/sync_vendor.py b/scripts/sync_vendor.py index 0ddef236a..658f7326b 100755 --- a/scripts/sync_vendor.py +++ b/scripts/sync_vendor.py @@ -5,7 +5,7 @@ import os import sys import subprocess -HTTPLIB_VERSION = "refs/tags/v0.44.0" +HTTPLIB_VERSION = "refs/tags/v0.45.0" vendor = { "https://github.com/nlohmann/json/releases/latest/download/json.hpp": "vendor/nlohmann/json.hpp", diff --git a/vendor/cpp-httplib/httplib.cpp b/vendor/cpp-httplib/httplib.cpp index ed9b09ba0..b28549607 100644 --- a/vendor/cpp-httplib/httplib.cpp +++ b/vendor/cpp-httplib/httplib.cpp @@ -4723,17 +4723,24 @@ write_multipart_ranges_data(Stream &strm, const Request &req, Response &res, }); } +bool has_framed_body(const Request &req) { + return is_chunked_transfer_encoding(req.headers) || + req.get_header_value_u64("Content-Length") > 0; +} + +bool is_connection_persistent(const Request &req) { + auto conn = req.get_header_value("Connection"); + if (conn == "close") { return false; } + if (req.version == "HTTP/1.0" && conn != "Keep-Alive") { return false; } + return true; +} + bool expect_content(const Request &req) { if (req.method == "POST" || req.method == "PUT" || req.method == "PATCH" || req.method == "DELETE") { return true; } - if (req.has_header("Content-Length") && - req.get_header_value_u64("Content-Length") > 0) { - return true; - } - if (is_chunked_transfer_encoding(req.headers)) { return true; } - return false; + return has_framed_body(req); } #ifdef _WIN32 @@ -7449,29 +7456,18 @@ bool Server::read_content_core( size_t /*len*/) { return receiver(buf, n); }; } - // RFC 7230 Section 3.3.3: If this is a request message and none of the above - // are true (no Transfer-Encoding and no Content-Length), then the message - // body length is zero (no message body is present). - // - // For non-SSL builds, detect clients that send a body without a - // Content-Length header (raw HTTP over TCP). Check both the stream's - // internal read buffer (data already read from the socket during header - // parsing) and the socket itself for pending data. If data is found and - // exceeds the configured payload limit, reject with 413. - // For SSL builds we cannot reliably peek the decrypted application bytes, - // so keep the original behaviour. + // RFC 9112 §6: no Transfer-Encoding and no Content-Length means no body. + // For non-SSL builds we still scan non-persistent connections for stray + // body bytes so the payload limit is enforced (413). On keep-alive, + // pending bytes may be the next request (issue #2450), so skip. #if !defined(CPPHTTPLIB_SSL_ENABLED) if (!req.has_header("Content-Length") && !detail::is_chunked_transfer_encoding(req.headers)) { - // Only check if payload_max_length is set to a finite value - if (payload_max_length_ > 0 && + if (!detail::is_connection_persistent(req) && payload_max_length_ > 0 && payload_max_length_ < (std::numeric_limits::max)()) { - // Check if there is data already buffered in the stream (read during - // header parsing) or pending on the socket. Use a non-blocking socket - // check to avoid deadlock when the client sends no body. - bool has_data = strm.is_readable(); + auto has_data = strm.is_readable(); if (!has_data) { - socket_t s = strm.socket(); + auto s = strm.socket(); if (s != INVALID_SOCKET) { has_data = detail::select_read(s, 0, 0) > 0; } @@ -8033,6 +8029,11 @@ get_client_ip(const std::string &x_forwarded_for, ip_list.emplace_back(std::string(b + r.first, b + r.second)); }); + // A malformed X-Forwarded-For (empty, comma-only, whitespace-only) yields + // no segments. Signal "no client IP derived" with an empty string so the + // caller can fall back to the connection-level remote address. + if (ip_list.empty()) { return std::string(); } + for (size_t i = 0; i < ip_list.size(); ++i) { auto ip = ip_list[i]; @@ -8123,7 +8124,8 @@ Server::process_request(Stream &strm, const std::string &remote_addr, if (!trusted_proxies_.empty() && req.has_header("X-Forwarded-For")) { auto x_forwarded_for = req.get_header_value("X-Forwarded-For"); - req.remote_addr = get_client_ip(x_forwarded_for, trusted_proxies_); + auto derived = get_client_ip(x_forwarded_for, trusted_proxies_); + req.remote_addr = derived.empty() ? remote_addr : derived; } else { req.remote_addr = remote_addr; } @@ -8325,15 +8327,14 @@ Server::process_request(Stream &strm, const std::string &remote_addr, ret = write_response(strm, close_connection, req, res); } - // Drain any unconsumed request body to prevent request smuggling on - // keep-alive connections. - if (!req.body_consumed_ && detail::expect_content(req)) { - int drain_status = 200; // required by read_content signature + // Drain any unconsumed framed body to prevent request smuggling on + // keep-alive. Without framing there is no body to drain — reading would + // consume the next request (issue #2450). + if (!req.body_consumed_ && detail::has_framed_body(req)) { + int dummy_status; if (!detail::read_content( - strm, req, payload_max_length_, drain_status, nullptr, + strm, req, payload_max_length_, dummy_status, nullptr, [](const char *, size_t, size_t, size_t) { return true; }, false)) { - // Body exceeds payload limit or read error — close the connection - // to prevent leftover bytes from being misinterpreted. connection_closed = true; } } diff --git a/vendor/cpp-httplib/httplib.h b/vendor/cpp-httplib/httplib.h index b954b94af..af750cdd9 100644 --- a/vendor/cpp-httplib/httplib.h +++ b/vendor/cpp-httplib/httplib.h @@ -8,8 +8,8 @@ #ifndef CPPHTTPLIB_HTTPLIB_H #define CPPHTTPLIB_HTTPLIB_H -#define CPPHTTPLIB_VERSION "0.44.0" -#define CPPHTTPLIB_VERSION_NUM "0x002c00" +#define CPPHTTPLIB_VERSION "0.45.0" +#define CPPHTTPLIB_VERSION_NUM "0x002d00" #ifdef _WIN32 #if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00