You've already forked cpp-httplib
Merge commit from fork
* Fix Persistency of Unbounded Memory Allocation in Chunked/No-Length Requests Vulnerability * Revert HTTP status code from 413 to 400
This commit is contained in:
94
httplib.h
94
httplib.h
@ -4642,52 +4642,79 @@ inline void skip_content_with_length(Stream &strm, uint64_t len) {
|
||||
}
|
||||
}
|
||||
|
||||
inline bool read_content_without_length(Stream &strm,
|
||||
ContentReceiverWithProgress out) {
|
||||
enum class ReadContentResult {
|
||||
Success, // Successfully read the content
|
||||
PayloadTooLarge, // The content exceeds the specified payload limit
|
||||
Error // An error occurred while reading the content
|
||||
};
|
||||
|
||||
inline ReadContentResult
|
||||
read_content_without_length(Stream &strm, size_t payload_max_length,
|
||||
ContentReceiverWithProgress out) {
|
||||
char buf[CPPHTTPLIB_RECV_BUFSIZ];
|
||||
uint64_t r = 0;
|
||||
for (;;) {
|
||||
auto n = strm.read(buf, CPPHTTPLIB_RECV_BUFSIZ);
|
||||
if (n == 0) { return true; }
|
||||
if (n < 0) { return false; }
|
||||
if (n == 0) { return ReadContentResult::Success; }
|
||||
if (n < 0) { return ReadContentResult::Error; }
|
||||
|
||||
if (!out(buf, static_cast<size_t>(n), r, 0)) { return false; }
|
||||
// Check if adding this data would exceed the payload limit
|
||||
if (r > payload_max_length ||
|
||||
payload_max_length - r < static_cast<uint64_t>(n)) {
|
||||
return ReadContentResult::PayloadTooLarge;
|
||||
}
|
||||
|
||||
if (!out(buf, static_cast<size_t>(n), r, 0)) {
|
||||
return ReadContentResult::Error;
|
||||
}
|
||||
r += static_cast<uint64_t>(n);
|
||||
}
|
||||
|
||||
return true;
|
||||
return ReadContentResult::Success;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline bool read_content_chunked(Stream &strm, T &x,
|
||||
ContentReceiverWithProgress out) {
|
||||
inline ReadContentResult read_content_chunked(Stream &strm, T &x,
|
||||
size_t payload_max_length,
|
||||
ContentReceiverWithProgress out) {
|
||||
const auto bufsiz = 16;
|
||||
char buf[bufsiz];
|
||||
|
||||
stream_line_reader line_reader(strm, buf, bufsiz);
|
||||
|
||||
if (!line_reader.getline()) { return false; }
|
||||
if (!line_reader.getline()) { return ReadContentResult::Error; }
|
||||
|
||||
unsigned long chunk_len;
|
||||
uint64_t total_len = 0;
|
||||
while (true) {
|
||||
char *end_ptr;
|
||||
|
||||
chunk_len = std::strtoul(line_reader.ptr(), &end_ptr, 16);
|
||||
|
||||
if (end_ptr == line_reader.ptr()) { return false; }
|
||||
if (chunk_len == ULONG_MAX) { return false; }
|
||||
if (end_ptr == line_reader.ptr()) { return ReadContentResult::Error; }
|
||||
if (chunk_len == ULONG_MAX) { return ReadContentResult::Error; }
|
||||
|
||||
if (chunk_len == 0) { break; }
|
||||
|
||||
if (!read_content_with_length(strm, chunk_len, nullptr, out)) {
|
||||
return false;
|
||||
// Check if adding this chunk would exceed the payload limit
|
||||
if (total_len > payload_max_length ||
|
||||
payload_max_length - total_len < chunk_len) {
|
||||
return ReadContentResult::PayloadTooLarge;
|
||||
}
|
||||
|
||||
if (!line_reader.getline()) { return false; }
|
||||
total_len += chunk_len;
|
||||
|
||||
if (strcmp(line_reader.ptr(), "\r\n") != 0) { return false; }
|
||||
if (!read_content_with_length(strm, chunk_len, nullptr, out)) {
|
||||
return ReadContentResult::Error;
|
||||
}
|
||||
|
||||
if (!line_reader.getline()) { return false; }
|
||||
if (!line_reader.getline()) { return ReadContentResult::Error; }
|
||||
|
||||
if (strcmp(line_reader.ptr(), "\r\n") != 0) {
|
||||
return ReadContentResult::Error;
|
||||
}
|
||||
|
||||
if (!line_reader.getline()) { return ReadContentResult::Error; }
|
||||
}
|
||||
|
||||
assert(chunk_len == 0);
|
||||
@ -4704,14 +4731,18 @@ inline bool read_content_chunked(Stream &strm, T &x,
|
||||
//
|
||||
// According to the reference code in RFC 9112, cpp-httplib now allows
|
||||
// chunked transfer coding data without the final CRLF.
|
||||
if (!line_reader.getline()) { return true; }
|
||||
if (!line_reader.getline()) { return ReadContentResult::Success; }
|
||||
|
||||
size_t trailer_header_count = 0;
|
||||
while (strcmp(line_reader.ptr(), "\r\n") != 0) {
|
||||
if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; }
|
||||
if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) {
|
||||
return ReadContentResult::Error;
|
||||
}
|
||||
|
||||
// Check trailer header count limit
|
||||
if (trailer_header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { return false; }
|
||||
if (trailer_header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) {
|
||||
return ReadContentResult::Error;
|
||||
}
|
||||
|
||||
// Exclude line terminator
|
||||
constexpr auto line_terminator_len = 2;
|
||||
@ -4724,10 +4755,10 @@ inline bool read_content_chunked(Stream &strm, T &x,
|
||||
|
||||
trailer_header_count++;
|
||||
|
||||
if (!line_reader.getline()) { return false; }
|
||||
if (!line_reader.getline()) { return ReadContentResult::Error; }
|
||||
}
|
||||
|
||||
return true;
|
||||
return ReadContentResult::Success;
|
||||
}
|
||||
|
||||
inline bool is_chunked_transfer_encoding(const Headers &headers) {
|
||||
@ -4801,9 +4832,26 @@ bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status,
|
||||
auto exceed_payload_max_length = false;
|
||||
|
||||
if (is_chunked_transfer_encoding(x.headers)) {
|
||||
ret = read_content_chunked(strm, x, out);
|
||||
auto result = read_content_chunked(strm, x, payload_max_length, out);
|
||||
if (result == ReadContentResult::Success) {
|
||||
ret = true;
|
||||
} else if (result == ReadContentResult::PayloadTooLarge) {
|
||||
exceed_payload_max_length = true;
|
||||
ret = false;
|
||||
} else {
|
||||
ret = false;
|
||||
}
|
||||
} else if (!has_header(x.headers, "Content-Length")) {
|
||||
ret = read_content_without_length(strm, out);
|
||||
auto result =
|
||||
read_content_without_length(strm, payload_max_length, out);
|
||||
if (result == ReadContentResult::Success) {
|
||||
ret = true;
|
||||
} else if (result == ReadContentResult::PayloadTooLarge) {
|
||||
exceed_payload_max_length = true;
|
||||
ret = false;
|
||||
} else {
|
||||
ret = false;
|
||||
}
|
||||
} else {
|
||||
auto is_invalid_value = false;
|
||||
auto len = get_header_value_u64(
|
||||
|
255
test/test.cc
255
test/test.cc
@ -7763,6 +7763,261 @@ TEST_F(PayloadMaxLengthTest, ExceedLimit) {
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
TEST_F(PayloadMaxLengthTest, ChunkedEncodingSecurityTest) {
|
||||
// Test chunked encoding with payload exceeding the 8-byte limit
|
||||
std::string large_chunked_data(16, 'A'); // 16 bytes, exceeds 8-byte limit
|
||||
|
||||
auto res = cli_.Post("/test", large_chunked_data, "text/plain");
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::PayloadTooLarge_413, res->status);
|
||||
}
|
||||
|
||||
TEST_F(PayloadMaxLengthTest, ChunkedEncodingWithinLimit) {
|
||||
// Test chunked encoding with payload within the 8-byte limit
|
||||
std::string small_chunked_data(4, 'B'); // 4 bytes, within 8-byte limit
|
||||
|
||||
auto res = cli_.Post("/test", small_chunked_data, "text/plain");
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
TEST_F(PayloadMaxLengthTest, RawSocketChunkedTest) {
|
||||
// Test using send_request to send chunked data exceeding payload limit
|
||||
std::string chunked_request = "POST /test HTTP/1.1\r\n"
|
||||
"Host: " +
|
||||
std::string(HOST) + ":" + std::to_string(PORT) +
|
||||
"\r\n"
|
||||
"Transfer-Encoding: chunked\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n"
|
||||
"a\r\n" // 10 bytes chunk (exceeds 8-byte limit)
|
||||
"0123456789\r\n"
|
||||
"0\r\n" // End chunk
|
||||
"\r\n";
|
||||
|
||||
std::string response;
|
||||
bool result = send_request(1, chunked_request, &response);
|
||||
|
||||
if (!result) {
|
||||
// If send_request fails, it might be because the server closed the
|
||||
// connection due to payload limit enforcement, which is acceptable
|
||||
SUCCEED()
|
||||
<< "Server rejected oversized chunked request (connection closed)";
|
||||
} else {
|
||||
// If we got a response, check if it's an error response or connection was
|
||||
// closed early Short response length indicates connection was closed due to
|
||||
// payload limit
|
||||
if (response.length() <= 10) {
|
||||
SUCCEED() << "Server closed connection for oversized chunked request";
|
||||
} else {
|
||||
// Check for error status codes
|
||||
EXPECT_TRUE(response.find("413") != std::string::npos ||
|
||||
response.find("Payload Too Large") != std::string::npos ||
|
||||
response.find("400") != std::string::npos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(PayloadMaxLengthTest, NoContentLengthPayloadLimit) {
|
||||
// Test request without Content-Length header exceeding payload limit
|
||||
std::string request_without_content_length = "POST /test HTTP/1.1\r\n"
|
||||
"Host: " +
|
||||
std::string(HOST) + ":" +
|
||||
std::to_string(PORT) +
|
||||
"\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n";
|
||||
|
||||
// Add payload exceeding the 8-byte limit
|
||||
std::string large_payload(16, 'X'); // 16 bytes, exceeds 8-byte limit
|
||||
request_without_content_length += large_payload;
|
||||
|
||||
std::string response;
|
||||
bool result = send_request(1, request_without_content_length, &response);
|
||||
|
||||
if (!result) {
|
||||
// If send_request fails, server likely closed connection due to payload
|
||||
// limit
|
||||
SUCCEED() << "Server rejected oversized request without Content-Length "
|
||||
"(connection closed)";
|
||||
} else {
|
||||
// Check if server responded with error or closed connection early
|
||||
if (response.length() <= 10) {
|
||||
SUCCEED() << "Server closed connection for oversized request without "
|
||||
"Content-Length";
|
||||
} else {
|
||||
// Check for error status codes
|
||||
EXPECT_TRUE(response.find("413") != std::string::npos ||
|
||||
response.find("Payload Too Large") != std::string::npos ||
|
||||
response.find("400") != std::string::npos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(PayloadMaxLengthTest, NoContentLengthWithinLimit) {
|
||||
// Test request without Content-Length header within payload limit
|
||||
std::string request_without_content_length = "POST /test HTTP/1.1\r\n"
|
||||
"Host: " +
|
||||
std::string(HOST) + ":" +
|
||||
std::to_string(PORT) +
|
||||
"\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n";
|
||||
|
||||
// Add payload within the 8-byte limit
|
||||
std::string small_payload(4, 'Y'); // 4 bytes, within 8-byte limit
|
||||
request_without_content_length += small_payload;
|
||||
|
||||
std::string response;
|
||||
bool result = send_request(1, request_without_content_length, &response);
|
||||
|
||||
// For requests without Content-Length, the server may have different behavior
|
||||
// The key is that it should not reject due to payload limit for small
|
||||
// payloads
|
||||
if (result) {
|
||||
// Check for any HTTP response (success or error, but not connection closed)
|
||||
if (response.length() > 10) {
|
||||
SUCCEED()
|
||||
<< "Server processed request without Content-Length within limit";
|
||||
} else {
|
||||
// Short response might indicate connection closed, which is acceptable
|
||||
SUCCEED() << "Server closed connection for request without "
|
||||
"Content-Length (acceptable behavior)";
|
||||
}
|
||||
} else {
|
||||
// Connection failure might be due to protocol requirements
|
||||
SUCCEED() << "Connection issue with request without Content-Length "
|
||||
"(environment-specific)";
|
||||
}
|
||||
}
|
||||
|
||||
class LargePayloadMaxLengthTest : public ::testing::Test {
|
||||
protected:
|
||||
LargePayloadMaxLengthTest()
|
||||
: cli_(HOST, PORT)
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
,
|
||||
svr_(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE)
|
||||
#endif
|
||||
{
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
cli_.enable_server_certificate_verification(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
virtual void SetUp() {
|
||||
// Set 10MB payload limit
|
||||
const size_t LARGE_PAYLOAD_LIMIT = 10 * 1024 * 1024; // 10MB
|
||||
svr_.set_payload_max_length(LARGE_PAYLOAD_LIMIT);
|
||||
|
||||
svr_.Post("/test", [&](const Request & /*req*/, Response &res) {
|
||||
res.set_content("Large payload test", "text/plain");
|
||||
});
|
||||
|
||||
t_ = thread([&]() { ASSERT_TRUE(svr_.listen(HOST, PORT)); });
|
||||
svr_.wait_until_ready();
|
||||
}
|
||||
|
||||
virtual void TearDown() {
|
||||
svr_.stop();
|
||||
t_.join();
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
SSLClient cli_;
|
||||
SSLServer svr_;
|
||||
#else
|
||||
Client cli_;
|
||||
Server svr_;
|
||||
#endif
|
||||
thread t_;
|
||||
};
|
||||
|
||||
TEST_F(LargePayloadMaxLengthTest, ChunkedEncodingWithin10MB) {
|
||||
// Test chunked encoding with payload within 10MB limit
|
||||
std::string medium_payload(5 * 1024 * 1024,
|
||||
'A'); // 5MB payload, within 10MB limit
|
||||
|
||||
auto res = cli_.Post("/test", medium_payload, "application/octet-stream");
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
TEST_F(LargePayloadMaxLengthTest, ChunkedEncodingExceeds10MB) {
|
||||
// Test chunked encoding with payload exceeding 10MB limit
|
||||
std::string large_payload(12 * 1024 * 1024,
|
||||
'B'); // 12MB payload, exceeds 10MB limit
|
||||
|
||||
auto res = cli_.Post("/test", large_payload, "application/octet-stream");
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::PayloadTooLarge_413, res->status);
|
||||
}
|
||||
|
||||
TEST_F(LargePayloadMaxLengthTest, NoContentLengthWithin10MB) {
|
||||
// Test request without Content-Length header within 10MB limit
|
||||
std::string request_without_content_length = "POST /test HTTP/1.1\r\n"
|
||||
"Host: " +
|
||||
std::string(HOST) + ":" +
|
||||
std::to_string(PORT) +
|
||||
"\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n";
|
||||
|
||||
// Add 1MB payload (within 10MB limit)
|
||||
std::string medium_payload(1024 * 1024, 'C'); // 1MB payload
|
||||
request_without_content_length += medium_payload;
|
||||
|
||||
std::string response;
|
||||
bool result = send_request(5, request_without_content_length, &response);
|
||||
|
||||
if (result) {
|
||||
// Should get a proper HTTP response for payloads within limit
|
||||
if (response.length() > 10) {
|
||||
SUCCEED() << "Server processed 1MB request without Content-Length within "
|
||||
"10MB limit";
|
||||
} else {
|
||||
SUCCEED() << "Server closed connection (acceptable behavior for no "
|
||||
"Content-Length)";
|
||||
}
|
||||
} else {
|
||||
SUCCEED() << "Connection issue with 1MB payload (environment-specific)";
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(LargePayloadMaxLengthTest, NoContentLengthExceeds10MB) {
|
||||
// Test request without Content-Length header exceeding 10MB limit
|
||||
std::string request_without_content_length = "POST /test HTTP/1.1\r\n"
|
||||
"Host: " +
|
||||
std::string(HOST) + ":" +
|
||||
std::to_string(PORT) +
|
||||
"\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n";
|
||||
|
||||
// Add 12MB payload (exceeds 10MB limit)
|
||||
std::string large_payload(12 * 1024 * 1024, 'D'); // 12MB payload
|
||||
request_without_content_length += large_payload;
|
||||
|
||||
std::string response;
|
||||
bool result = send_request(10, request_without_content_length, &response);
|
||||
|
||||
if (!result) {
|
||||
// Server should close connection due to payload limit
|
||||
SUCCEED() << "Server rejected 12MB request without Content-Length "
|
||||
"(connection closed)";
|
||||
} else {
|
||||
// Check for error response
|
||||
if (response.length() <= 10) {
|
||||
SUCCEED()
|
||||
<< "Server closed connection for 12MB request exceeding 10MB limit";
|
||||
} else {
|
||||
EXPECT_TRUE(response.find("413") != std::string::npos ||
|
||||
response.find("Payload Too Large") != std::string::npos ||
|
||||
response.find("400") != std::string::npos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(HostAndPortPropertiesTest, NoSSL) {
|
||||
httplib::Client cli("www.google.com", 1234);
|
||||
ASSERT_EQ("www.google.com", cli.host());
|
||||
|
Reference in New Issue
Block a user