diff --git a/docs/mkdocs/docs/api/basic_json/to_bson.md b/docs/mkdocs/docs/api/basic_json/to_bson.md index cb199dc32..6374d4c86 100644 --- a/docs/mkdocs/docs/api/basic_json/to_bson.md +++ b/docs/mkdocs/docs/api/basic_json/to_bson.md @@ -40,6 +40,9 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va is not an object; example: `"to serialize to BSON, top-level type must be object, but is string"` - Throws [`out_of_range.409`](../../home/exceptions.md#jsonexceptionout_of_range409) if a key in the JSON object contains a null byte (code point U+0000); example: `"BSON key cannot contain code point U+0000 (at byte 2)"` +- Throws [`out_of_range.412`](../../home/exceptions.md#jsonexceptionout_of_range412) if the length of a document, array, + string, or binary value exceeds the range of the 32-bit BSON length field; example: + `"BSON length 2147483661 exceeds maximum of 2147483647"` ## Complexity diff --git a/docs/mkdocs/docs/home/exceptions.md b/docs/mkdocs/docs/home/exceptions.md index 859ebeb48..235744009 100644 --- a/docs/mkdocs/docs/home/exceptions.md +++ b/docs/mkdocs/docs/home/exceptions.md @@ -898,6 +898,22 @@ A JSON Patch `add` operation cannot be applied because the target location's par This exception was added in version 3.13.0. Before that, this situation hit an internal assertion (aborting the program in debug builds) or was silently ignored when assertions were disabled. +### json.exception.out_of_range.412 + +BSON stores the length of documents, arrays, strings, and binary values in a signed 32-bit integer. This exception is thrown when a value is too large to be described by such a length field. + +!!! failure "Example message" + + ``` + BSON length 2147483661 exceeds maximum of 2147483647 + ``` + +!!! note + + This exception was added in version 3.13.0. Before that, the length was silently truncated, and + [`to_bson`](../api/basic_json/to_bson.md) produced documents with negative length prefixes that + [`from_bson`](../api/basic_json/from_bson.md) rejected. + ## Further exceptions This exception is thrown in case of errors that cannot be classified with the diff --git a/include/nlohmann/detail/output/binary_writer.hpp b/include/nlohmann/detail/output/binary_writer.hpp index 72735c338..2d79c193f 100644 --- a/include/nlohmann/detail/output/binary_writer.hpp +++ b/include/nlohmann/detail/output/binary_writer.hpp @@ -979,6 +979,21 @@ class binary_writer return /*id*/ 1ul + name.size() + /*zero-terminator*/1u; } + /*! + @brief Checks that @a size fits into the 32-bit length field used by BSON + @return The size as a signed 32-bit integer + @throw out_of_range.412 if @a size exceeds the range of std::int32_t + */ + static std::int32_t to_bson_length(const std::size_t size) + { + if (JSON_HEDLEY_UNLIKELY(!value_in_range_of(size))) + { + JSON_THROW(out_of_range::create(412, concat("BSON length ", std::to_string(size), " exceeds maximum of ", std::to_string((std::numeric_limits::max)())), nullptr)); + } + + return static_cast(size); + } + /*! @brief Writes the given @a element_type and @a name to the output adapter */ @@ -1027,7 +1042,7 @@ class binary_writer { write_bson_entry_header(name, 0x02); - write_number(static_cast(value.size() + 1ul), true); + write_number(to_bson_length(value.size() + 1ul), true); oa->write_characters( reinterpret_cast(value.c_str()), value.size() + 1); @@ -1142,7 +1157,7 @@ class binary_writer const typename BasicJsonType::array_t& value) { write_bson_entry_header(name, 0x04); // array - write_number(static_cast(calc_bson_array_size(value)), true); + write_number(to_bson_length(calc_bson_array_size(value)), true); std::size_t array_index = 0ul; @@ -1162,7 +1177,7 @@ class binary_writer { write_bson_entry_header(name, 0x05); - write_number(static_cast(value.size()), true); + write_number(to_bson_length(value.size()), true); write_number(value.has_subtype() ? static_cast(value.subtype()) : static_cast(0x00)); oa->write_characters(reinterpret_cast(value.data()), value.size()); @@ -1284,7 +1299,7 @@ class binary_writer */ void write_bson_object(const typename BasicJsonType::object_t& value) { - write_number(static_cast(calc_bson_object_size(value)), true); + write_number(to_bson_length(calc_bson_object_size(value)), true); for (const auto& el : value) { diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 821863109..5ead5275a 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -17764,6 +17764,21 @@ class binary_writer return /*id*/ 1ul + name.size() + /*zero-terminator*/1u; } + /*! + @brief Checks that @a size fits into the 32-bit length field used by BSON + @return The size as a signed 32-bit integer + @throw out_of_range.412 if @a size exceeds the range of std::int32_t + */ + static std::int32_t to_bson_length(const std::size_t size) + { + if (JSON_HEDLEY_UNLIKELY(!value_in_range_of(size))) + { + JSON_THROW(out_of_range::create(412, concat("BSON length ", std::to_string(size), " exceeds maximum of ", std::to_string((std::numeric_limits::max)())), nullptr)); + } + + return static_cast(size); + } + /*! @brief Writes the given @a element_type and @a name to the output adapter */ @@ -17812,7 +17827,7 @@ class binary_writer { write_bson_entry_header(name, 0x02); - write_number(static_cast(value.size() + 1ul), true); + write_number(to_bson_length(value.size() + 1ul), true); oa->write_characters( reinterpret_cast(value.c_str()), value.size() + 1); @@ -17927,7 +17942,7 @@ class binary_writer const typename BasicJsonType::array_t& value) { write_bson_entry_header(name, 0x04); // array - write_number(static_cast(calc_bson_array_size(value)), true); + write_number(to_bson_length(calc_bson_array_size(value)), true); std::size_t array_index = 0ul; @@ -17947,7 +17962,7 @@ class binary_writer { write_bson_entry_header(name, 0x05); - write_number(static_cast(value.size()), true); + write_number(to_bson_length(value.size()), true); write_number(value.has_subtype() ? static_cast(value.subtype()) : static_cast(0x00)); oa->write_characters(reinterpret_cast(value.data()), value.size()); @@ -18069,7 +18084,7 @@ class binary_writer */ void write_bson_object(const typename BasicJsonType::object_t& value) { - write_number(static_cast(calc_bson_object_size(value)), true); + write_number(to_bson_length(calc_bson_object_size(value)), true); for (const auto& el : value) { diff --git a/tests/src/unit-bson.cpp b/tests/src/unit-bson.cpp index 87344b9b1..fbd83faad 100644 --- a/tests/src/unit-bson.cpp +++ b/tests/src/unit-bson.cpp @@ -11,12 +11,35 @@ #include using nlohmann::json; +#include #include #include #include +#include #include "make_test_data_available.hpp" #include "test_utils.hpp" +namespace +{ +// a binary container that reports a size beyond INT32_MAX without allocating +// that much memory, so the BSON length overflow can be tested cheaply +class huge_binary_t : public std::vector +{ + public: + using std::vector::vector; + + size_type size() const noexcept // NOLINT(readability-convert-member-functions-to-static) + { + // one byte more than the BSON length field can represent + return static_cast((std::numeric_limits::max)()) + 1; + } +}; + +using huge_binary_json = nlohmann::basic_json < + std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, + double, std::allocator, nlohmann::adl_serializer, huge_binary_t, void >; +} // namespace + TEST_CASE("BSON") { SECTION("individual values not supported") @@ -80,6 +103,14 @@ TEST_CASE("BSON") #endif } + SECTION("lengths exceeding INT32_MAX cannot be serialized to BSON") + { + huge_binary_json j; + j["b"] = huge_binary_json::binary(huge_binary_t{}); + + CHECK_THROWS_WITH_AS(huge_binary_json::to_bson(j), "[json.exception.out_of_range.412] BSON length 2147483661 exceeds maximum of 2147483647", huge_binary_json::out_of_range&); + } + SECTION("string length must be at least 1") { // from https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=11175