Skip to content

Commit 2e23687

Browse files
authored
to_bson() silently emits corrupt documents when a length exceeds INT32_MAX (#5314)
1 parent 227c5cd commit 2e23687

5 files changed

Lines changed: 88 additions & 8 deletions

File tree

docs/mkdocs/docs/api/basic_json/to_bson.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ Strong guarantee: if an exception is thrown, there are no changes in the JSON va
4040
is not an object; example: `"to serialize to BSON, top-level type must be object, but is string"`
4141
- Throws [`out_of_range.409`](../../home/exceptions.md#jsonexceptionout_of_range409) if a key in the JSON object contains
4242
a null byte (code point U+0000); example: `"BSON key cannot contain code point U+0000 (at byte 2)"`
43+
- Throws [`out_of_range.412`](../../home/exceptions.md#jsonexceptionout_of_range412) if the length of a document, array,
44+
string, or binary value exceeds the range of the 32-bit BSON length field; example:
45+
`"BSON length 2147483661 exceeds maximum of 2147483647"`
4346
4447
## Complexity
4548

docs/mkdocs/docs/home/exceptions.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,6 +898,22 @@ A JSON Patch `add` operation cannot be applied because the target location's par
898898

899899
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.
900900

901+
### json.exception.out_of_range.412
902+
903+
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.
904+
905+
!!! failure "Example message"
906+
907+
```
908+
BSON length 2147483661 exceeds maximum of 2147483647
909+
```
910+
911+
!!! note
912+
913+
This exception was added in version 3.13.0. Before that, the length was silently truncated, and
914+
[`to_bson`](../api/basic_json/to_bson.md) produced documents with negative length prefixes that
915+
[`from_bson`](../api/basic_json/from_bson.md) rejected.
916+
901917
## Further exceptions
902918

903919
This exception is thrown in case of errors that cannot be classified with the

include/nlohmann/detail/output/binary_writer.hpp

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -979,6 +979,21 @@ class binary_writer
979979
return /*id*/ 1ul + name.size() + /*zero-terminator*/1u;
980980
}
981981

982+
/*!
983+
@brief Checks that @a size fits into the 32-bit length field used by BSON
984+
@return The size as a signed 32-bit integer
985+
@throw out_of_range.412 if @a size exceeds the range of std::int32_t
986+
*/
987+
static std::int32_t to_bson_length(const std::size_t size)
988+
{
989+
if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::int32_t>(size)))
990+
{
991+
JSON_THROW(out_of_range::create(412, concat("BSON length ", std::to_string(size), " exceeds maximum of ", std::to_string((std::numeric_limits<std::int32_t>::max)())), nullptr));
992+
}
993+
994+
return static_cast<std::int32_t>(size);
995+
}
996+
982997
/*!
983998
@brief Writes the given @a element_type and @a name to the output adapter
984999
*/
@@ -1027,7 +1042,7 @@ class binary_writer
10271042
{
10281043
write_bson_entry_header(name, 0x02);
10291044

1030-
write_number<std::int32_t>(static_cast<std::int32_t>(value.size() + 1ul), true);
1045+
write_number<std::int32_t>(to_bson_length(value.size() + 1ul), true);
10311046
oa->write_characters(
10321047
reinterpret_cast<const CharType*>(value.c_str()),
10331048
value.size() + 1);
@@ -1142,7 +1157,7 @@ class binary_writer
11421157
const typename BasicJsonType::array_t& value)
11431158
{
11441159
write_bson_entry_header(name, 0x04); // array
1145-
write_number<std::int32_t>(static_cast<std::int32_t>(calc_bson_array_size(value)), true);
1160+
write_number<std::int32_t>(to_bson_length(calc_bson_array_size(value)), true);
11461161

11471162
std::size_t array_index = 0ul;
11481163

@@ -1162,7 +1177,7 @@ class binary_writer
11621177
{
11631178
write_bson_entry_header(name, 0x05);
11641179

1165-
write_number<std::int32_t>(static_cast<std::int32_t>(value.size()), true);
1180+
write_number<std::int32_t>(to_bson_length(value.size()), true);
11661181
write_number(value.has_subtype() ? static_cast<std::uint8_t>(value.subtype()) : static_cast<std::uint8_t>(0x00));
11671182

11681183
oa->write_characters(reinterpret_cast<const CharType*>(value.data()), value.size());
@@ -1284,7 +1299,7 @@ class binary_writer
12841299
*/
12851300
void write_bson_object(const typename BasicJsonType::object_t& value)
12861301
{
1287-
write_number<std::int32_t>(static_cast<std::int32_t>(calc_bson_object_size(value)), true);
1302+
write_number<std::int32_t>(to_bson_length(calc_bson_object_size(value)), true);
12881303

12891304
for (const auto& el : value)
12901305
{

single_include/nlohmann/json.hpp

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17764,6 +17764,21 @@ class binary_writer
1776417764
return /*id*/ 1ul + name.size() + /*zero-terminator*/1u;
1776517765
}
1776617766

17767+
/*!
17768+
@brief Checks that @a size fits into the 32-bit length field used by BSON
17769+
@return The size as a signed 32-bit integer
17770+
@throw out_of_range.412 if @a size exceeds the range of std::int32_t
17771+
*/
17772+
static std::int32_t to_bson_length(const std::size_t size)
17773+
{
17774+
if (JSON_HEDLEY_UNLIKELY(!value_in_range_of<std::int32_t>(size)))
17775+
{
17776+
JSON_THROW(out_of_range::create(412, concat("BSON length ", std::to_string(size), " exceeds maximum of ", std::to_string((std::numeric_limits<std::int32_t>::max)())), nullptr));
17777+
}
17778+
17779+
return static_cast<std::int32_t>(size);
17780+
}
17781+
1776717782
/*!
1776817783
@brief Writes the given @a element_type and @a name to the output adapter
1776917784
*/
@@ -17812,7 +17827,7 @@ class binary_writer
1781217827
{
1781317828
write_bson_entry_header(name, 0x02);
1781417829

17815-
write_number<std::int32_t>(static_cast<std::int32_t>(value.size() + 1ul), true);
17830+
write_number<std::int32_t>(to_bson_length(value.size() + 1ul), true);
1781617831
oa->write_characters(
1781717832
reinterpret_cast<const CharType*>(value.c_str()),
1781817833
value.size() + 1);
@@ -17927,7 +17942,7 @@ class binary_writer
1792717942
const typename BasicJsonType::array_t& value)
1792817943
{
1792917944
write_bson_entry_header(name, 0x04); // array
17930-
write_number<std::int32_t>(static_cast<std::int32_t>(calc_bson_array_size(value)), true);
17945+
write_number<std::int32_t>(to_bson_length(calc_bson_array_size(value)), true);
1793117946

1793217947
std::size_t array_index = 0ul;
1793317948

@@ -17947,7 +17962,7 @@ class binary_writer
1794717962
{
1794817963
write_bson_entry_header(name, 0x05);
1794917964

17950-
write_number<std::int32_t>(static_cast<std::int32_t>(value.size()), true);
17965+
write_number<std::int32_t>(to_bson_length(value.size()), true);
1795117966
write_number(value.has_subtype() ? static_cast<std::uint8_t>(value.subtype()) : static_cast<std::uint8_t>(0x00));
1795217967

1795317968
oa->write_characters(reinterpret_cast<const CharType*>(value.data()), value.size());
@@ -18069,7 +18084,7 @@ class binary_writer
1806918084
*/
1807018085
void write_bson_object(const typename BasicJsonType::object_t& value)
1807118086
{
18072-
write_number<std::int32_t>(static_cast<std::int32_t>(calc_bson_object_size(value)), true);
18087+
write_number<std::int32_t>(to_bson_length(calc_bson_object_size(value)), true);
1807318088

1807418089
for (const auto& el : value)
1807518090
{

tests/src/unit-bson.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,35 @@
1111
#include <nlohmann/json.hpp>
1212
using nlohmann::json;
1313

14+
#include <cstdint>
1415
#include <fstream>
1516
#include <limits>
1617
#include <sstream>
18+
#include <vector>
1719
#include "make_test_data_available.hpp"
1820
#include "test_utils.hpp"
1921

22+
namespace
23+
{
24+
// a binary container that reports a size beyond INT32_MAX without allocating
25+
// that much memory, so the BSON length overflow can be tested cheaply
26+
class huge_binary_t : public std::vector<std::uint8_t>
27+
{
28+
public:
29+
using std::vector<std::uint8_t>::vector;
30+
31+
size_type size() const noexcept // NOLINT(readability-convert-member-functions-to-static)
32+
{
33+
// one byte more than the BSON length field can represent
34+
return static_cast<size_type>((std::numeric_limits<std::int32_t>::max)()) + 1;
35+
}
36+
};
37+
38+
using huge_binary_json = nlohmann::basic_json <
39+
std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t,
40+
double, std::allocator, nlohmann::adl_serializer, huge_binary_t, void >;
41+
} // namespace
42+
2043
TEST_CASE("BSON")
2144
{
2245
SECTION("individual values not supported")
@@ -80,6 +103,14 @@ TEST_CASE("BSON")
80103
#endif
81104
}
82105

106+
SECTION("lengths exceeding INT32_MAX cannot be serialized to BSON")
107+
{
108+
huge_binary_json j;
109+
j["b"] = huge_binary_json::binary(huge_binary_t{});
110+
111+
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&);
112+
}
113+
83114
SECTION("string length must be at least 1")
84115
{
85116
// from https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=11175

0 commit comments

Comments
 (0)