Skip to content

Commit 7653032

Browse files
committed
CXX-1234 Validate GridFS input from user and server
Covers cases of: - Invalid options to database::gridfs_bucket(). - Invalid options to bucket::open_upload_stream(). - Invalid files document on server. - Invalid chunks document on server.
1 parent fac948c commit 7653032

File tree

4 files changed

+252
-7
lines changed

4 files changed

+252
-7
lines changed

src/mongocxx/gridfs/bucket.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ bucket::bucket(const database& db, const options::gridfs::bucket& options) {
5151
default_chunk_size_bytes = *chunk_size_bytes;
5252
}
5353

54+
if (default_chunk_size_bytes <= 0) {
55+
throw logic_error{error_code::k_invalid_parameter,
56+
"positive value for chunk_size_bytes required"};
57+
}
58+
5459
collection chunks = db[bucket_name + ".chunks"];
5560
collection files = db[bucket_name + ".files"];
5661

@@ -111,6 +116,12 @@ uploader bucket::open_upload_stream_with_id(bsoncxx::types::value id,
111116
std::int32_t chunk_size_bytes = _get_impl().default_chunk_size_bytes;
112117

113118
if (auto chunk_size = options.chunk_size_bytes()) {
119+
if (*chunk_size <= 0) {
120+
throw logic_error{
121+
error_code::k_invalid_parameter,
122+
"positive value required for options::gridfs::upload::chunk_size_bytes()"};
123+
}
124+
114125
chunk_size_bytes = *chunk_size;
115126
}
116127

src/mongocxx/gridfs/bucket.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ class MONGOCXX_API bucket {
116116
/// If this GridFS bucket does not already exist in the database, it will be implicitly
117117
/// created and initialized with GridFS indexes.
118118
///
119+
/// @throws mongocxx::logic_error if `options` are invalid.
120+
///
119121
/// @throws mongocxx::query_exception
120122
/// if an error occurs when reading from the files collection for this bucket.
121123
///
@@ -144,6 +146,8 @@ class MONGOCXX_API bucket {
144146
/// If this GridFS bucket does not already exist in the database, it will be implicitly
145147
/// created and initialized with GridFS indexes.
146148
///
149+
/// @throws mongocxx::logic_error if `options` are invalid.
150+
///
147151
/// @throws mongocxx::query_exception
148152
/// if an error occurs when reading from the files collection for this bucket.
149153
///
@@ -174,6 +178,8 @@ class MONGOCXX_API bucket {
174178
/// If this GridFS bucket does not already exist in the database, it will be implicitly
175179
/// created and initialized with GridFS indexes.
176180
///
181+
/// @throws mongocxx::logic_error if `options` are invalid.
182+
///
177183
/// @throws mongocxx::bulk_write_exception
178184
/// if an error occurs when writing chunk data or file metadata to the database.
179185
///
@@ -215,6 +221,8 @@ class MONGOCXX_API bucket {
215221
/// If this GridFS bucket does not already exist in the database, it will be implicitly
216222
/// created and initialized with GridFS indexes.
217223
///
224+
/// @throws mongocxx::logic_error if `options` are invalid.
225+
///
218226
/// @throws mongocxx::bulk_write_exception
219227
/// if an error occurs when writing chunk data or file metadata to the database.
220228
///

src/mongocxx/gridfs/private/downloader.hh

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,24 @@ namespace gridfs {
2828
namespace {
2929
std::int64_t read_length_from_files_document(bsoncxx::document::view files_doc) {
3030
auto length_ele = files_doc["length"];
31+
std::int64_t length;
3132
if (length_ele && length_ele.type() == bsoncxx::type::k_int64) {
32-
return length_ele.get_int64().value;
33+
length = length_ele.get_int64().value;
3334
} else if (length_ele && length_ele.type() == bsoncxx::type::k_int32) {
34-
return length_ele.get_int32().value;
35+
length = length_ele.get_int32().value;
36+
} else {
37+
throw gridfs_exception{error_code::k_gridfs_file_corrupted,
38+
"expected files document to contain field \"length\" with type "
39+
"k_int32 or k_int64"};
3540
}
3641

37-
throw gridfs_exception{error_code::k_gridfs_file_corrupted,
38-
"expected files document to contain field \"length\" with type "
39-
"k_int32 or k_int64"};
42+
if (length < 0) {
43+
std::ostringstream err;
44+
err << "files document contains unexpected negative value for \"length\": " << length;
45+
throw gridfs_exception{error_code::k_gridfs_file_corrupted, err.str()};
46+
}
47+
48+
return length;
4049
}
4150

4251
std::int32_t read_chunk_size_from_files_document(bsoncxx::document::view files_doc) {
@@ -58,8 +67,13 @@ std::int32_t read_chunk_size_from_files_document(bsoncxx::document::view files_d
5867
// Each chunk needs to be able to fit in a single document.
5968
if (chunk_size > k_max_document_size) {
6069
std::ostringstream err;
61-
err << "file has chunk size of " << chunk_size << ", which exceeds maximum chunk size of "
62-
<< k_max_document_size;
70+
err << "files document contains unexpected chunk size of " << chunk_size
71+
<< ", which exceeds maximum chunk size of " << k_max_document_size;
72+
throw gridfs_exception{error_code::k_gridfs_file_corrupted, err.str()};
73+
} else if (chunk_size <= 0) {
74+
std::ostringstream err;
75+
err << "files document contains unexpected chunk size: " << chunk_size
76+
<< "; value must be positive";
6377
throw gridfs_exception{error_code::k_gridfs_file_corrupted, err.str()};
6478
}
6579

src/mongocxx/test/gridfs/bucket.cpp

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include <bsoncxx/types/value.hpp>
2727
#include <mongocxx/client.hpp>
2828
#include <mongocxx/database.hpp>
29+
#include <mongocxx/exception/gridfs_exception.hpp>
2930
#include <mongocxx/exception/logic_error.hpp>
3031
#include <mongocxx/gridfs/bucket.hpp>
3132
#include <mongocxx/instance.hpp>
@@ -103,6 +104,217 @@ TEST_CASE("mongocxx::gridfs::bucket copy assignment operator", "[gridfs::bucket]
103104
}
104105
}
105106

107+
TEST_CASE("database::gridfs_bucket() throws error when options are invalid", "[gridfs::bucket]") {
108+
instance::current();
109+
110+
client client{uri{}};
111+
database db = client["gridfs_bucket_error_invalid_options"];
112+
113+
options::gridfs::bucket bucket_options;
114+
115+
SECTION("empty bucket name") {
116+
bucket_options.bucket_name("");
117+
}
118+
SECTION("zero chunk size") {
119+
bucket_options.chunk_size_bytes(0);
120+
}
121+
SECTION("negative chunk size") {
122+
bucket_options.chunk_size_bytes(-1);
123+
}
124+
125+
REQUIRE_THROWS_AS(db.gridfs_bucket(bucket_options), logic_error);
126+
}
127+
128+
TEST_CASE("uploading throws error when options are invalid", "[gridfs::bucket]") {
129+
instance::current();
130+
131+
client client{uri{}};
132+
database db = client["gridfs_upload_error_invalid_options"];
133+
gridfs::bucket bucket = db.gridfs_bucket();
134+
135+
options::gridfs::upload upload_options;
136+
137+
SECTION("zero chunk size") {
138+
upload_options.chunk_size_bytes(0);
139+
}
140+
SECTION("negative chunk size") {
141+
upload_options.chunk_size_bytes(-1);
142+
}
143+
144+
REQUIRE_THROWS_AS(bucket.open_upload_stream("filename", upload_options), logic_error);
145+
REQUIRE_THROWS_AS(
146+
bucket.open_upload_stream_with_id(
147+
bsoncxx::types::value{bsoncxx::types::b_int32{0}}, "filename", upload_options),
148+
logic_error);
149+
150+
std::istringstream iss{"foo"};
151+
REQUIRE_THROWS_AS(bucket.upload_from_stream("filename", &iss, upload_options), logic_error);
152+
REQUIRE_THROWS_AS(
153+
bucket.upload_from_stream_with_id(
154+
bsoncxx::types::value{bsoncxx::types::b_int32{0}}, "filename", &iss, upload_options),
155+
logic_error);
156+
}
157+
158+
TEST_CASE("downloading throws error when files document is corrupt", "[gridfs::bucket]") {
159+
instance::current();
160+
161+
client client{uri{}};
162+
database db = client["gridfs_files_doc_corrupt"];
163+
gridfs::bucket bucket = db.gridfs_bucket();
164+
165+
db["fs.files"].drop();
166+
167+
const std::int32_t k_expected_chunk_size_bytes = 255 * 1024; // Default chunk size.
168+
const std::int64_t k_expected_file_length = 1024 * 1024;
169+
stdx::optional<bsoncxx::types::value> chunk_size{
170+
bsoncxx::types::value{bsoncxx::types::b_int32{k_expected_chunk_size_bytes}}};
171+
stdx::optional<bsoncxx::types::value> length{
172+
bsoncxx::types::value{bsoncxx::types::b_int64{k_expected_file_length}}};
173+
bool expect_success = false;
174+
175+
// Tests for invalid chunk size.
176+
SECTION("zero chunk size") {
177+
chunk_size = bsoncxx::types::value{bsoncxx::types::b_int32{0}};
178+
}
179+
SECTION("negative chunk size") {
180+
chunk_size = bsoncxx::types::value{bsoncxx::types::b_int32{-1}};
181+
}
182+
SECTION("chunk size too large") {
183+
const std::int32_t k_max_document_size = 16 * 1024 * 1024;
184+
chunk_size = bsoncxx::types::value{bsoncxx::types::b_int32{k_max_document_size + 1}};
185+
}
186+
SECTION("chunk size of wrong type") {
187+
chunk_size = bsoncxx::types::value{bsoncxx::types::b_utf8{"invalid"}};
188+
}
189+
SECTION("missing chunk size") {
190+
chunk_size = stdx::nullopt;
191+
}
192+
193+
// Tests for invalid length.
194+
SECTION("negative length") {
195+
length = bsoncxx::types::value{bsoncxx::types::b_int64{-1}};
196+
}
197+
SECTION("invalid length") {
198+
length = bsoncxx::types::value{bsoncxx::types::b_utf8{"invalid"}};
199+
}
200+
SECTION("missing length") {
201+
length = stdx::nullopt;
202+
}
203+
204+
// Test for too many chunks.
205+
SECTION("too many chunks") {
206+
chunk_size = bsoncxx::types::value{bsoncxx::types::b_int32{1}};
207+
length = bsoncxx::types::value{bsoncxx::types::b_int64{
208+
static_cast<std::int64_t>(std::numeric_limits<std::int32_t>::max()) + 1}};
209+
}
210+
211+
// Test for valid length and chunk size.
212+
SECTION("valid length and chunk size") {
213+
expect_success = true;
214+
}
215+
216+
{
217+
bsoncxx::builder::basic::document files_doc;
218+
files_doc.append(kvp("_id", 0));
219+
if (length) {
220+
files_doc.append(kvp("length", *length));
221+
}
222+
if (chunk_size) {
223+
files_doc.append(kvp("chunkSize", *chunk_size));
224+
}
225+
226+
db["fs.files"].insert_one(files_doc.extract());
227+
}
228+
229+
auto open_download_stream = [&bucket]() {
230+
return bucket.open_download_stream(bsoncxx::types::value{bsoncxx::types::b_int32{0}});
231+
};
232+
233+
if (expect_success) {
234+
gridfs::downloader downloader = open_download_stream();
235+
REQUIRE(downloader.chunk_size() == k_expected_chunk_size_bytes);
236+
REQUIRE(downloader.file_length() == k_expected_file_length);
237+
} else {
238+
REQUIRE_THROWS_AS(open_download_stream(), gridfs_exception);
239+
}
240+
}
241+
242+
TEST_CASE("downloading throws error when chunks document is corrupt", "[gridfs::bucket]") {
243+
instance::current();
244+
245+
client client{uri{}};
246+
database db = client["gridfs_chunk_doc_corrupt"];
247+
gridfs::bucket bucket = db.gridfs_bucket();
248+
249+
db["fs.files"].drop();
250+
db["fs.chunks"].drop();
251+
252+
const std::uint8_t k_expected_data_byte = 'd';
253+
254+
stdx::optional<bsoncxx::types::value> n{bsoncxx::types::value{bsoncxx::types::b_int32{0}}};
255+
stdx::optional<bsoncxx::types::value> data{bsoncxx::types::value{
256+
bsoncxx::types::b_binary{bsoncxx::binary_sub_type::k_binary, 1, &k_expected_data_byte}}};
257+
bool expect_success = false;
258+
259+
// Tests for invalid n.
260+
SECTION("missing n") {
261+
n = stdx::nullopt;
262+
}
263+
SECTION("wrong type for n") {
264+
n = bsoncxx::types::value{bsoncxx::types::b_int64{0}};
265+
}
266+
267+
// Tests for invalid data.
268+
SECTION("missing data") {
269+
data = stdx::nullopt;
270+
}
271+
SECTION("wrong type for data") {
272+
data = bsoncxx::types::value{bsoncxx::types::b_bool{true}};
273+
}
274+
275+
// Test for valid n and data.
276+
SECTION("valid n and data") {
277+
expect_success = true;
278+
}
279+
280+
{
281+
bsoncxx::builder::basic::document chunk_doc;
282+
chunk_doc.append(kvp("files_id", 0));
283+
if (n) {
284+
chunk_doc.append(kvp("n", *n));
285+
}
286+
if (data) {
287+
chunk_doc.append(kvp("data", *data));
288+
}
289+
290+
db["fs.chunks"].insert_one(chunk_doc.extract());
291+
db["fs.files"].insert_one(
292+
make_document(kvp("_id", 0), kvp("length", 1), kvp("chunkSize", 1)));
293+
}
294+
295+
gridfs::downloader downloader =
296+
bucket.open_download_stream(bsoncxx::types::value{bsoncxx::types::b_int32{0}});
297+
auto downloader_read_one = [&downloader]() {
298+
stdx::optional<std::uint8_t> result;
299+
std::uint8_t byte;
300+
std::size_t bytes_downloaded = downloader.read(1, &byte);
301+
if (bytes_downloaded > 0) {
302+
result = byte;
303+
}
304+
return result;
305+
};
306+
307+
if (expect_success) {
308+
stdx::optional<std::uint8_t> result = downloader_read_one();
309+
REQUIRE(result);
310+
REQUIRE(*result == k_expected_data_byte);
311+
result = downloader_read_one();
312+
REQUIRE(!result);
313+
} else {
314+
REQUIRE_THROWS_AS(downloader_read_one(), gridfs_exception);
315+
}
316+
}
317+
106318
// Add an arbitrary GridFS file with a specified length, chunk size, and id to a database's default
107319
// GridFS bucket (i.e. the "fs.files" and "fs.chunks" collections).
108320
//

0 commit comments

Comments
 (0)