diff --git a/include/nlohmann/detail/input/binary_reader.hpp b/include/nlohmann/detail/input/binary_reader.hpp index 4f8ab79f2..ad862827d 100644 --- a/include/nlohmann/detail/input/binary_reader.hpp +++ b/include/nlohmann/detail/input/binary_reader.hpp @@ -163,12 +163,39 @@ class binary_reader // BSON // ////////// + /*! + @brief Validate a BSON document's declared size against the bytes read. + + A BSON document starts with an int32 that counts its own total length in + bytes, including that prefix and the trailing 0x00. The reader is driven + by the terminator rather than the declared length, so without this check a + nested document could declare a length that disagrees with where its + terminator actually falls and quietly hand the bytes in between to the + enclosing document. A well-formed document is at least 5 bytes (the prefix + plus the terminator); the equality also rejects those impossible sizes, + since at least 5 bytes are always consumed. + + @param[in] document_start value of chars_read before the size prefix + @param[in] document_size the declared document size + @return whether the declared size matches the number of bytes read + */ + bool check_bson_document_size(const std::size_t document_start, const std::int32_t document_size) + { + if (JSON_HEDLEY_UNLIKELY(document_size < 0 || static_cast(document_size) != chars_read - document_start)) + { + return sax->parse_error(chars_read, get_token_string(), parse_error::create(112, chars_read, + exception_message(input_format_t::bson, concat("document size ", std::to_string(document_size), " does not match the number of bytes read (", std::to_string(chars_read - document_start), ")"), "document"), nullptr)); + } + return true; + } + /*! @brief Reads in a BSON-object and passes it to the SAX-parser. @return whether a valid BSON-value was passed to the SAX parser */ bool parse_bson_internal() { + const std::size_t document_start = chars_read; std::int32_t document_size{}; get_number(input_format_t::bson, document_size); @@ -182,6 +209,11 @@ class binary_reader return false; } + if (JSON_HEDLEY_UNLIKELY(!check_bson_document_size(document_start, document_size))) + { + return false; + } + return sax->end_object(); } @@ -397,6 +429,7 @@ class binary_reader */ bool parse_bson_array() { + const std::size_t document_start = chars_read; std::int32_t document_size{}; get_number(input_format_t::bson, document_size); @@ -410,6 +443,11 @@ class binary_reader return false; } + if (JSON_HEDLEY_UNLIKELY(!check_bson_document_size(document_start, document_size))) + { + return false; + } + return sax->end_array(); } diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 580b9bd02..485935212 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -10712,12 +10712,39 @@ class binary_reader // BSON // ////////// + /*! + @brief Validate a BSON document's declared size against the bytes read. + + A BSON document starts with an int32 that counts its own total length in + bytes, including that prefix and the trailing 0x00. The reader is driven + by the terminator rather than the declared length, so without this check a + nested document could declare a length that disagrees with where its + terminator actually falls and quietly hand the bytes in between to the + enclosing document. A well-formed document is at least 5 bytes (the prefix + plus the terminator); the equality also rejects those impossible sizes, + since at least 5 bytes are always consumed. + + @param[in] document_start value of chars_read before the size prefix + @param[in] document_size the declared document size + @return whether the declared size matches the number of bytes read + */ + bool check_bson_document_size(const std::size_t document_start, const std::int32_t document_size) + { + if (JSON_HEDLEY_UNLIKELY(document_size < 0 || static_cast(document_size) != chars_read - document_start)) + { + return sax->parse_error(chars_read, get_token_string(), parse_error::create(112, chars_read, + exception_message(input_format_t::bson, concat("document size ", std::to_string(document_size), " does not match the number of bytes read (", std::to_string(chars_read - document_start), ")"), "document"), nullptr)); + } + return true; + } + /*! @brief Reads in a BSON-object and passes it to the SAX-parser. @return whether a valid BSON-value was passed to the SAX parser */ bool parse_bson_internal() { + const std::size_t document_start = chars_read; std::int32_t document_size{}; get_number(input_format_t::bson, document_size); @@ -10731,6 +10758,11 @@ class binary_reader return false; } + if (JSON_HEDLEY_UNLIKELY(!check_bson_document_size(document_start, document_size))) + { + return false; + } + return sax->end_object(); } @@ -10946,6 +10978,7 @@ class binary_reader */ bool parse_bson_array() { + const std::size_t document_start = chars_read; std::int32_t document_size{}; get_number(input_format_t::bson, document_size); @@ -10959,6 +10992,11 @@ class binary_reader return false; } + if (JSON_HEDLEY_UNLIKELY(!check_bson_document_size(document_start, document_size))) + { + return false; + } + return sax->end_array(); } diff --git a/tests/src/unit-bson.cpp b/tests/src/unit-bson.cpp index 28837d75e..87344b9b1 100644 --- a/tests/src/unit-bson.cpp +++ b/tests/src/unit-bson.cpp @@ -854,6 +854,62 @@ TEST_CASE("Unsupported BSON input") CHECK(!json::sax_parse(bson, &scp, json::input_format_t::bson)); } +TEST_CASE("BSON document size mismatch") +{ + json _; + + SECTION("top-level document declaring more bytes than it contains") + { + // empty object, but the length prefix claims 6 bytes instead of 5 + std::vector const input = {0x06, 0x00, 0x00, 0x00, 0x00}; + CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 5: syntax error while parsing BSON document: document size 6 does not match the number of bytes read (5)", json::parse_error&); + CHECK(json::from_bson(input, true, false).is_discarded()); + } + + SECTION("top-level document with a negative size") + { + std::vector const input = {0xFF, 0xFF, 0xFF, 0xFF, 0x00}; + CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 5: syntax error while parsing BSON document: document size -1 does not match the number of bytes read (5)", json::parse_error&); + CHECK(json::from_bson(input, true, false).is_discarded()); + } + + SECTION("embedded document whose size disagrees with its terminator") + { + // the embedded document "d" declares 0x7FFFFFFF bytes but its 0x00 + // terminator falls right after {"a":null}; the length prefix would + // otherwise let the following "h" element be read as a member of the + // enclosing document instead of "d" + std::vector const input = + { + 0x00, 0x00, 0x00, 0x00, // outer size + 0x03, 'd', 0x00, // entry: embedded document "d" + 0xFF, 0xFF, 0xFF, 0x7F, // embedded size 0x7FFFFFFF + 0x0A, 'a', 0x00, // entry: null "a" + 0x00, // embedded end marker + 0x08, 'h', 0x00, 0x01, // entry: bool "h" = true + 0x00 // outer end marker + }; + CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 15: syntax error while parsing BSON document: document size 2147483647 does not match the number of bytes read (8)", json::parse_error&); + CHECK(json::from_bson(input, true, false).is_discarded()); + } + + SECTION("embedded array whose size disagrees with its terminator") + { + // array [42] is 12 bytes, but the length prefix claims 13 + std::vector const input = + { + 0x00, 0x00, 0x00, 0x00, // outer size + 0x04, 'a', 0x00, // entry: array "a" + 0x0D, 0x00, 0x00, 0x00, // array size 13 (real is 12) + 0x10, '0', 0x00, 0x2A, 0x00, 0x00, 0x00, // entry: int32 "0" = 42 + 0x00, // array end marker + 0x00 // outer end marker + }; + CHECK_THROWS_WITH_AS(_ = json::from_bson(input), "[json.exception.parse_error.112] parse error at byte 19: syntax error while parsing BSON document: document size 13 does not match the number of bytes read (12)", json::parse_error&); + CHECK(json::from_bson(input, true, false).is_discarded()); + } +} + TEST_CASE("BSON numerical data") { SECTION("number")