mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-05 14:53:48 +07:00
reject ambiguously framed responses in client read paths (#2581)
* reject ambiguously framed responses in client read paths * Accept non-chunked Transfer-Encoding responses in the client framing guard RFC 9112 §6.3 treats requests and responses differently when the final transfer coding is not chunked: a request's body length cannot be determined and the server must answer 400, but a response's body simply runs until the server closes the connection. read_content() and the open_stream() body reader already do that, so such a response is not ambiguous and rejecting it broke valid responses such as "Transfer-Encoding: gzip" followed by a close. Keep rejecting a Transfer-Encoding paired with a non-zero Content-Length, which is the actual ambiguity, and drop the non-chunked clause from both client read paths. Tests: check that rejection surfaces as Error::Read, that a non-chunked Transfer-Encoding response is read until close on both paths, and that HEAD, 204 and 304 responses with both framing headers are not rejected. Claude-Session: https://claude.ai/code/session_01JYPWKpbp4a881EdpEf2xSi * Share the framing check and reuse existing test helpers Factor "Transfer-Encoding with a non-zero Content-Length" into detail::has_conflicting_content_length() next to is_chunked_transfer_encoding(), and call it from the server request guard and both client read paths so the rule and its RFC 9112 §6.3 rationale live in one place. In the tests, drop the POSIX-only raw socket helper in favour of the existing serve_single_response() and read_all(), which also lets the tests run on Windows. Fold the stream-only test into the buffered one so each case checks both Get() and open_stream(), and cover the HEAD/204/304 exclusion on the open_stream() path too. Claude-Session: https://claude.ai/code/session_01JYPWKpbp4a881EdpEf2xSi --------- Co-authored-by: yhirose <yuji.hirose.bug@gmail.com>
This commit is contained in:
@@ -19863,6 +19863,105 @@ TEST(OpenStreamMalformedContentLength, OutOfRange) {
|
||||
server_thread.join();
|
||||
}
|
||||
|
||||
// Serves `response` to the single request `fn` makes with a fresh client.
|
||||
template <typename Fn>
|
||||
static void with_single_response(const std::string &response, Fn fn) {
|
||||
#ifndef _WIN32
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
#endif
|
||||
|
||||
std::promise<int> port_promise;
|
||||
auto port_future = port_promise.get_future();
|
||||
auto server_thread = serve_single_response(port_promise, response);
|
||||
auto se = detail::scope_exit([&] { server_thread.join(); });
|
||||
|
||||
auto port = port_future.get();
|
||||
ASSERT_GT(port, 0);
|
||||
Client cli("127.0.0.1", port);
|
||||
fn(cli);
|
||||
}
|
||||
|
||||
// RFC 9112 §6.3: a response that pairs a non-zero Content-Length with
|
||||
// Transfer-Encoding is framed ambiguously. Both read paths delimit its body by
|
||||
// the chunked coding and drop Content-Length, so a front-end that trusts
|
||||
// Content-Length would disagree about where the body ends (response
|
||||
// smuggling). The client must reject such a response.
|
||||
TEST(ClientResponseSmugglingTest, ContentLengthAndTransferEncodingRejected) {
|
||||
for (const char *te : {"chunked", "gzip, chunked"}) {
|
||||
auto response = std::string("HTTP/1.1 200 OK\r\n") +
|
||||
"Content-Length: 5\r\n" + "Transfer-Encoding: " + te +
|
||||
"\r\n" + "Connection: close\r\n" + "\r\n" +
|
||||
"5\r\nhello\r\n0\r\n\r\n";
|
||||
|
||||
with_single_response(response, [&](Client &cli) {
|
||||
auto res = cli.Get("/");
|
||||
EXPECT_FALSE(static_cast<bool>(res)) << te;
|
||||
EXPECT_EQ(Error::Read, res.error()) << te;
|
||||
});
|
||||
|
||||
with_single_response(response, [&](Client &cli) {
|
||||
auto handle = cli.open_stream("GET", "/");
|
||||
EXPECT_FALSE(handle.is_valid()) << te;
|
||||
EXPECT_EQ(Error::Read, handle.error) << te;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Unambiguous framing stays readable on both paths: chunked alone, and a final
|
||||
// coding other than chunked, whose body RFC 9112 §6.3 delimits by the server
|
||||
// closing the connection (unlike a request, which must be rejected).
|
||||
TEST(ClientResponseSmugglingTest, UnambiguousFramingAccepted) {
|
||||
for (const char *response : {"HTTP/1.1 200 OK\r\n"
|
||||
"Transfer-Encoding: chunked\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n"
|
||||
"5\r\nhello\r\n0\r\n\r\n",
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"Transfer-Encoding: gzip\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n"
|
||||
"hello"}) {
|
||||
with_single_response(response, [&](Client &cli) {
|
||||
auto res = cli.Get("/");
|
||||
ASSERT_TRUE(static_cast<bool>(res)) << response;
|
||||
EXPECT_EQ("hello", res->body);
|
||||
});
|
||||
|
||||
with_single_response(response, [&](Client &cli) {
|
||||
auto handle = cli.open_stream("GET", "/");
|
||||
ASSERT_TRUE(handle.is_valid()) << response;
|
||||
EXPECT_EQ("hello", read_all(handle));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// A response to HEAD, and a 204 or 304 response, carries no body, so its
|
||||
// framing headers describe nothing to read and must not be rejected.
|
||||
TEST(ClientResponseSmugglingTest, BodylessResponseNotRejected) {
|
||||
const std::string framing = "Content-Length: 5\r\n"
|
||||
"Transfer-Encoding: chunked\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n";
|
||||
|
||||
with_single_response("HTTP/1.1 200 OK\r\n" + framing, [](Client &cli) {
|
||||
EXPECT_TRUE(static_cast<bool>(cli.Head("/")));
|
||||
});
|
||||
with_single_response("HTTP/1.1 200 OK\r\n" + framing, [](Client &cli) {
|
||||
EXPECT_TRUE(cli.open_stream("HEAD", "/").is_valid());
|
||||
});
|
||||
|
||||
for (const char *status : {"204 No Content", "304 Not Modified"}) {
|
||||
auto response = std::string("HTTP/1.1 ") + status + "\r\n" + framing;
|
||||
|
||||
with_single_response(response, [&](Client &cli) {
|
||||
EXPECT_TRUE(static_cast<bool>(cli.Get("/"))) << status;
|
||||
});
|
||||
with_single_response(response, [&](Client &cli) {
|
||||
EXPECT_TRUE(cli.open_stream("GET", "/").is_valid()) << status;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
|
||||
TEST_F(OpenStreamTest, Gzip) {
|
||||
Client cli("127.0.0.1", port_);
|
||||
|
||||
Reference in New Issue
Block a user