mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-29 12:50:51 +00:00
Merge commit from fork
This commit is contained in:
1 parent
defd907c74
commit
c99d7472b5
2 files changed
+124
-30
No files matched your search
@@ -6950,7 +6950,8 @@ inline bool is_chunked_transfer_encoding(const Headers &headers) {
|
|||||||
template <typename T, typename U>
|
template <typename T, typename U>
|
||||||
bool prepare_content_receiver(T &x, int &status,
|
bool prepare_content_receiver(T &x, int &status,
|
||||||
ContentReceiverWithProgress receiver,
|
ContentReceiverWithProgress receiver,
|
||||||
bool decompress, U callback) {
|
bool decompress, size_t payload_max_length,
|
||||||
|
bool &exceed_payload_max_length, U callback) {
|
||||||
if (decompress) {
|
if (decompress) {
|
||||||
std::string encoding = x.get_header_value("Content-Encoding");
|
std::string encoding = x.get_header_value("Content-Encoding");
|
||||||
std::unique_ptr<decompressor> decompressor;
|
std::unique_ptr<decompressor> decompressor;
|
||||||
@@ -6966,12 +6967,22 @@ bool prepare_content_receiver(T &x, int &status,
|
|||||||
|
|
||||||
if (decompressor) {
|
if (decompressor) {
|
||||||
if (decompressor->is_valid()) {
|
if (decompressor->is_valid()) {
|
||||||
|
size_t decompressed_size = 0;
|
||||||
ContentReceiverWithProgress out = [&](const char *buf, size_t n,
|
ContentReceiverWithProgress out = [&](const char *buf, size_t n,
|
||||||
size_t off, size_t len) {
|
size_t off, size_t len) {
|
||||||
return decompressor->decompress(buf, n,
|
return decompressor->decompress(
|
||||||
[&](const char *buf2, size_t n2) {
|
buf, n, [&](const char *buf2, size_t n2) {
|
||||||
return receiver(buf2, n2, off, len);
|
// Guard against zip-bomb: check
|
||||||
});
|
// decompressed size against limit.
|
||||||
|
if (payload_max_length > 0 &&
|
||||||
|
(decompressed_size >= payload_max_length ||
|
||||||
|
n2 > payload_max_length - decompressed_size)) {
|
||||||
|
exceed_payload_max_length = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
decompressed_size += n2;
|
||||||
|
return receiver(buf2, n2, off, len);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
return callback(std::move(out));
|
return callback(std::move(out));
|
||||||
} else {
|
} else {
|
||||||
@@ -6992,11 +7003,14 @@ template <typename T>
|
|||||||
bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status,
|
bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status,
|
||||||
DownloadProgress progress,
|
DownloadProgress progress,
|
||||||
ContentReceiverWithProgress receiver, bool decompress) {
|
ContentReceiverWithProgress receiver, bool decompress) {
|
||||||
|
bool exceed_payload_max_length = false;
|
||||||
return prepare_content_receiver(
|
return prepare_content_receiver(
|
||||||
x, status, std::move(receiver), decompress,
|
x, status, std::move(receiver), decompress, payload_max_length,
|
||||||
[&](const ContentReceiverWithProgress &out) {
|
exceed_payload_max_length, [&](const ContentReceiverWithProgress &out) {
|
||||||
auto ret = true;
|
auto ret = true;
|
||||||
auto exceed_payload_max_length = false;
|
// Note: exceed_payload_max_length may also be set by the decompressor
|
||||||
|
// wrapper in prepare_content_receiver when the decompressed payload
|
||||||
|
// size exceeds the limit.
|
||||||
|
|
||||||
if (is_chunked_transfer_encoding(x.headers)) {
|
if (is_chunked_transfer_encoding(x.headers)) {
|
||||||
auto result = read_content_chunked(strm, x, payload_max_length, out);
|
auto result = read_content_chunked(strm, x, payload_max_length, out);
|
||||||
@@ -11288,45 +11302,63 @@ inline bool Server::routing(Request &req, Response &res, Stream &strm) {
|
|||||||
if (detail::expect_content(req)) {
|
if (detail::expect_content(req)) {
|
||||||
// Content reader handler
|
// Content reader handler
|
||||||
{
|
{
|
||||||
|
// Track whether the ContentReader was aborted due to the decompressed
|
||||||
|
// payload exceeding `payload_max_length_`.
|
||||||
|
// The user handler runs after the lambda returns, so we must restore the
|
||||||
|
// 413 status if the handler overwrites it.
|
||||||
|
bool content_reader_payload_too_large = false;
|
||||||
|
|
||||||
ContentReader reader(
|
ContentReader reader(
|
||||||
[&](ContentReceiver receiver) {
|
[&](ContentReceiver receiver) {
|
||||||
auto result = read_content_with_content_receiver(
|
auto result = read_content_with_content_receiver(
|
||||||
strm, req, res, std::move(receiver), nullptr, nullptr);
|
strm, req, res, std::move(receiver), nullptr, nullptr);
|
||||||
if (!result) { output_error_log(Error::Read, &req); }
|
if (!result) {
|
||||||
|
output_error_log(Error::Read, &req);
|
||||||
|
if (res.status == StatusCode::PayloadTooLarge_413) {
|
||||||
|
content_reader_payload_too_large = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
[&](FormDataHeader header, ContentReceiver receiver) {
|
[&](FormDataHeader header, ContentReceiver receiver) {
|
||||||
auto result = read_content_with_content_receiver(
|
auto result = read_content_with_content_receiver(
|
||||||
strm, req, res, nullptr, std::move(header),
|
strm, req, res, nullptr, std::move(header),
|
||||||
std::move(receiver));
|
std::move(receiver));
|
||||||
if (!result) { output_error_log(Error::Read, &req); }
|
if (!result) {
|
||||||
|
output_error_log(Error::Read, &req);
|
||||||
|
if (res.status == StatusCode::PayloadTooLarge_413) {
|
||||||
|
content_reader_payload_too_large = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bool dispatched = false;
|
||||||
if (req.method == "POST") {
|
if (req.method == "POST") {
|
||||||
if (dispatch_request_for_content_reader(
|
dispatched = dispatch_request_for_content_reader(
|
||||||
req, res, std::move(reader),
|
req, res, std::move(reader), post_handlers_for_content_reader_);
|
||||||
post_handlers_for_content_reader_)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else if (req.method == "PUT") {
|
} else if (req.method == "PUT") {
|
||||||
if (dispatch_request_for_content_reader(
|
dispatched = dispatch_request_for_content_reader(
|
||||||
req, res, std::move(reader),
|
req, res, std::move(reader), put_handlers_for_content_reader_);
|
||||||
put_handlers_for_content_reader_)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else if (req.method == "PATCH") {
|
} else if (req.method == "PATCH") {
|
||||||
if (dispatch_request_for_content_reader(
|
dispatched = dispatch_request_for_content_reader(
|
||||||
req, res, std::move(reader),
|
req, res, std::move(reader), patch_handlers_for_content_reader_);
|
||||||
patch_handlers_for_content_reader_)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else if (req.method == "DELETE") {
|
} else if (req.method == "DELETE") {
|
||||||
if (dispatch_request_for_content_reader(
|
dispatched = dispatch_request_for_content_reader(
|
||||||
req, res, std::move(reader),
|
req, res, std::move(reader), delete_handlers_for_content_reader_);
|
||||||
delete_handlers_for_content_reader_)) {
|
}
|
||||||
return true;
|
|
||||||
|
if (dispatched) {
|
||||||
|
if (content_reader_payload_too_large) {
|
||||||
|
// Enforce the limit: override any status the handler may have set
|
||||||
|
// and return false so the error path sends a plain 413 response.
|
||||||
|
res.status = StatusCode::PayloadTooLarge_413;
|
||||||
|
res.body.clear();
|
||||||
|
res.content_length_ = 0;
|
||||||
|
res.content_provider_ = nullptr;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+63
-1
@@ -8649,6 +8649,68 @@ TEST_F(LargePayloadMaxLengthTest, NoContentLengthExceeds10MB) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
|
||||||
|
// `payload_max_length` is not enforced on decompressed body in ContentReader
|
||||||
|
// path.
|
||||||
|
TEST(PayloadLimitBypassTest, StreamingGzipDecompression) {
|
||||||
|
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 thread = std::thread([&]() { svr.listen(HOST, PORT); });
|
||||||
|
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
|
||||||
|
std::string raw(256 * 1024, 'A');
|
||||||
|
std::string gz;
|
||||||
|
{
|
||||||
|
z_stream zs{};
|
||||||
|
deflateInit2(&zs, Z_BEST_COMPRESSION, Z_DEFLATED, 15 + 16, 8,
|
||||||
|
Z_DEFAULT_STRATEGY);
|
||||||
|
zs.next_in = reinterpret_cast<Bytef *>(const_cast<char *>(raw.data()));
|
||||||
|
zs.avail_in = static_cast<uInt>(raw.size());
|
||||||
|
char outbuf[4096];
|
||||||
|
int ret;
|
||||||
|
do {
|
||||||
|
zs.next_out = reinterpret_cast<Bytef *>(outbuf);
|
||||||
|
zs.avail_out = sizeof(outbuf);
|
||||||
|
ret = deflate(&zs, Z_FINISH);
|
||||||
|
gz.append(outbuf, sizeof(outbuf) - zs.avail_out);
|
||||||
|
} while (ret != Z_STREAM_END);
|
||||||
|
deflateEnd(&zs);
|
||||||
|
}
|
||||||
|
|
||||||
|
Client cli(HOST, PORT);
|
||||||
|
cli.set_connection_timeout(std::chrono::seconds(5));
|
||||||
|
Headers headers = {{"Content-Encoding", "gzip"}};
|
||||||
|
auto res = cli.Post("/stream", headers, gz.data(), gz.size(),
|
||||||
|
"application/octet-stream");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
|
||||||
|
// Server must reject oversized decompressed payloads with 413.
|
||||||
|
EXPECT_EQ(StatusCode::PayloadTooLarge_413, res->status);
|
||||||
|
|
||||||
|
// Decompressed bytes delivered to the handler must not exceed LIMIT.
|
||||||
|
EXPECT_LE(total, LIMIT);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// Regression test for DoS vulnerability: a malicious server sending a response
|
// Regression test for DoS vulnerability: a malicious server sending a response
|
||||||
// without Content-Length header must not cause unbounded memory consumption on
|
// without Content-Length header must not cause unbounded memory consumption on
|
||||||
// the client side. The client should stop reading after a reasonable limit,
|
// the client side. The client should stop reading after a reasonable limit,
|
||||||
@@ -15658,7 +15720,7 @@ TEST(ZipBombProtectionTest, DecompressedSizeExceedsLimit) {
|
|||||||
|
|
||||||
// Server should reject because decompressed size (8KB) exceeds limit (1KB)
|
// Server should reject because decompressed size (8KB) exceeds limit (1KB)
|
||||||
ASSERT_TRUE(res);
|
ASSERT_TRUE(res);
|
||||||
EXPECT_EQ(StatusCode::BadRequest_400, res->status);
|
EXPECT_EQ(StatusCode::PayloadTooLarge_413, res->status);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user