Enforce payload_max_length on decompressed size for unframed requests

For a non-SSL request with neither Content-Length nor Transfer-Encoding,
Server::read_content_core() fell back to reading raw wire bytes with
detail::read_content_without_length() directly, bypassing the decompressor
wrapper that the length-framed and chunked paths already use. As a result,
payload_max_length only bounded the compressed bytes read off the socket,
not the decompressed size a handler could produce from them.

Route this fallback through detail::read_content(..., decompress=true)
instead, the same helper already used below for the length-framed and
chunked cases, so the decompressed-size guard applies uniformly.
This commit is contained in:
yhirose
2026-08-13 23:35:30 -04:00
parent f00e476f1b
commit 89e0c5c238
2 changed files with 71 additions and 9 deletions
+65
View File
@@ -10898,6 +10898,71 @@ TEST(PayloadLimitBypassTest, StreamingGzipDecompression) {
// Decompressed bytes delivered to the handler must not exceed LIMIT.
EXPECT_LE(total, LIMIT);
}
#ifndef CPPHTTPLIB_SSL_ENABLED
// For a request with neither Content-Length nor Transfer-Encoding, the
// non-SSL raw-socket fallback in read_content_core() used to hand the
// compressed wire bytes straight to the ContentReader without ever routing
// them through the decompressor, so payload_max_length_ only bounded the
// compressed size on the wire, not the decompressed size.
TEST(PayloadLimitBypassTest, UnframedGzipDecompression) {
Server svr;
const size_t LIMIT = 64 * 1024; // 64KB
svr.set_payload_max_length(LIMIT);
size_t total = 0;
svr.Post("/stream", [&](const Request & /*req*/, Response &res,
const ContentReader &content_reader) {
content_reader([&](const char * /*data*/, size_t len) {
total += len;
return true;
});
res.status = 200;
res.set_content("stream_ok", "text/plain");
});
auto port = svr.bind_to_any_port(HOST);
auto thread = std::thread([&]() { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
svr.stop();
thread.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
// Prepare 256KB raw data and gzip-compress it, same payload as the
// StreamingGzipDecompression test above.
std::string raw(256 * 1024, 'A');
std::string gz;
{
httplib::detail::gzip_compressor compressor;
bool result = compressor.compress(raw.data(), raw.size(), /*last=*/true,
[&](const char *chunk, size_t len) {
gz.append(chunk, len);
return true;
});
ASSERT_TRUE(result);
}
// Send the gzip body over raw TCP with neither Content-Length nor
// Transfer-Encoding, and Connection: close so the server's stray-body scan
// kicks in.
std::string req = "POST /stream HTTP/1.1\r\n"
"Host: " +
std::string(HOST) + ":" + std::to_string(port) +
"\r\n"
"Content-Encoding: gzip\r\n"
"Connection: close\r\n"
"\r\n" +
gz;
std::string response;
ASSERT_TRUE(send_request(5, req, &response, port));
// Decompressed bytes delivered to the handler must not exceed LIMIT.
EXPECT_LE(total, LIMIT);
}
#endif
#endif
// Some servers misuse Content-Encoding to advertise a character set, e.g.