diff --git a/include/nlohmann/detail/input/binary_reader.hpp b/include/nlohmann/detail/input/binary_reader.hpp index 033cfebd78..63dfffa9ca 100644 --- a/include/nlohmann/detail/input/binary_reader.hpp +++ b/include/nlohmann/detail/input/binary_reader.hpp @@ -170,7 +170,24 @@ class binary_reader bool parse_bson_internal() { std::int32_t document_size{}; - get_number(input_format_t::bson, document_size); + if (JSON_HEDLEY_UNLIKELY((!get_number(input_format_t::bson, document_size)))) + { + return false; + } + + if (JSON_HEDLEY_UNLIKELY(document_size < 5)) + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, + exception_message(input_format_t::bson, + concat("BSON document size must be at least 5, is ", std::to_string(document_size)), + "document size"), nullptr)); + } + + // The document begins at the size field and ends document_size bytes later + // (including the size field itself and the trailing 0x00 terminator). + const std::size_t document_start = chars_read - sizeof(std::int32_t); + const std::size_t expected_end = document_start + static_cast(document_size); if (JSON_HEDLEY_UNLIKELY(!sax->start_object(detail::unknown_size()))) { @@ -182,6 +199,15 @@ class binary_reader return false; } + if (JSON_HEDLEY_UNLIKELY(chars_read != expected_end)) + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, + exception_message(input_format_t::bson, + "BSON document terminator did not land at declared document size", + "document"), nullptr)); + } + return sax->end_object(); } @@ -231,7 +257,21 @@ class binary_reader exception_message(input_format_t::bson, concat("string length must be at least 1, is ", std::to_string(len)), "string"), nullptr)); } - return get_string(input_format_t::bson, len - static_cast(1), result) && get() != char_traits::eof(); + if (JSON_HEDLEY_UNLIKELY(!get_string(input_format_t::bson, len - static_cast(1), result))) + { + return false; + } + + if (JSON_HEDLEY_UNLIKELY(get() != 0x00)) + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, + exception_message(input_format_t::bson, + "BSON string is not null-terminated", + "string"), nullptr)); + } + + return true; } /*! @@ -398,7 +438,24 @@ class binary_reader bool parse_bson_array() { std::int32_t document_size{}; - get_number(input_format_t::bson, document_size); + if (JSON_HEDLEY_UNLIKELY((!get_number(input_format_t::bson, document_size)))) + { + return false; + } + + if (JSON_HEDLEY_UNLIKELY(document_size < 5)) + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, + exception_message(input_format_t::bson, + concat("BSON document size must be at least 5, is ", std::to_string(document_size)), + "document size"), nullptr)); + } + + // The document begins at the size field and ends document_size bytes later + // (including the size field itself and the trailing 0x00 terminator). + const std::size_t document_start = chars_read - sizeof(std::int32_t); + const std::size_t expected_end = document_start + static_cast(document_size); if (JSON_HEDLEY_UNLIKELY(!sax->start_array(detail::unknown_size()))) { @@ -410,6 +467,15 @@ class binary_reader return false; } + if (JSON_HEDLEY_UNLIKELY(chars_read != expected_end)) + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, + exception_message(input_format_t::bson, + "BSON array terminator did not land at declared array size", + "array"), nullptr)); + } + return sax->end_array(); } diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 2e16ad5b9e..411bb01efe 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -10312,7 +10312,24 @@ class binary_reader bool parse_bson_internal() { std::int32_t document_size{}; - get_number(input_format_t::bson, document_size); + if (JSON_HEDLEY_UNLIKELY((!get_number(input_format_t::bson, document_size)))) + { + return false; + } + + if (JSON_HEDLEY_UNLIKELY(document_size < 5)) + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, + exception_message(input_format_t::bson, + concat("BSON document size must be at least 5, is ", std::to_string(document_size)), + "document size"), nullptr)); + } + + // The document begins at the size field and ends document_size bytes later + // (including the size field itself and the trailing 0x00 terminator). + const std::size_t document_start = chars_read - sizeof(std::int32_t); + const std::size_t expected_end = document_start + static_cast(document_size); if (JSON_HEDLEY_UNLIKELY(!sax->start_object(detail::unknown_size()))) { @@ -10324,6 +10341,15 @@ class binary_reader return false; } + if (JSON_HEDLEY_UNLIKELY(chars_read != expected_end)) + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, + exception_message(input_format_t::bson, + "BSON document terminator did not land at declared document size", + "document"), nullptr)); + } + return sax->end_object(); } @@ -10373,7 +10399,21 @@ class binary_reader exception_message(input_format_t::bson, concat("string length must be at least 1, is ", std::to_string(len)), "string"), nullptr)); } - return get_string(input_format_t::bson, len - static_cast(1), result) && get() != char_traits::eof(); + if (JSON_HEDLEY_UNLIKELY(!get_string(input_format_t::bson, len - static_cast(1), result))) + { + return false; + } + + if (JSON_HEDLEY_UNLIKELY(get() != 0x00)) + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, + exception_message(input_format_t::bson, + "BSON string is not null-terminated", + "string"), nullptr)); + } + + return true; } /*! @@ -10540,7 +10580,24 @@ class binary_reader bool parse_bson_array() { std::int32_t document_size{}; - get_number(input_format_t::bson, document_size); + if (JSON_HEDLEY_UNLIKELY((!get_number(input_format_t::bson, document_size)))) + { + return false; + } + + if (JSON_HEDLEY_UNLIKELY(document_size < 5)) + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, + exception_message(input_format_t::bson, + concat("BSON document size must be at least 5, is ", std::to_string(document_size)), + "document size"), nullptr)); + } + + // The document begins at the size field and ends document_size bytes later + // (including the size field itself and the trailing 0x00 terminator). + const std::size_t document_start = chars_read - sizeof(std::int32_t); + const std::size_t expected_end = document_start + static_cast(document_size); if (JSON_HEDLEY_UNLIKELY(!sax->start_array(detail::unknown_size()))) { @@ -10552,6 +10609,15 @@ class binary_reader return false; } + if (JSON_HEDLEY_UNLIKELY(chars_read != expected_end)) + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, + exception_message(input_format_t::bson, + "BSON array terminator did not land at declared array size", + "array"), nullptr)); + } + return sax->end_array(); } diff --git a/tests/src/unit-bson.cpp b/tests/src/unit-bson.cpp index eed70352e8..e801250fa3 100644 --- a/tests/src/unit-bson.cpp +++ b/tests/src/unit-bson.cpp @@ -1294,3 +1294,76 @@ TEST_CASE("BSON roundtrips" * doctest::skip()) } } } + +TEST_CASE("Invalid document size handling") +{ + SECTION("document size must be at least 5") + { + std::vector const v = {0x04, 0x00, 0x00, 0x00, 0x00}; + json _; + CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 4: syntax error while parsing BSON document size: BSON document size must be at least 5, is 4", json::parse_error&); + CHECK(json::from_bson(v, true, false).is_discarded()); + } + + SECTION("declared document size must match consumed bytes (extra trailing element)") + { + // Declares 5-byte empty document but appends an int32 element after the declared end. + std::vector const v = + { + 0x05, 0x00, 0x00, 0x00, + 0x10, 'a', 'd', 'm', 'i', 'n', 0x00, + 0x01, 0x00, 0x00, 0x00, + 0x00 + }; + json _; + CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 16: syntax error while parsing BSON document: BSON document terminator did not land at declared document size", json::parse_error&); + CHECK(json::from_bson(v, true, false).is_discarded()); + } + + SECTION("declared document size must match consumed bytes (premature terminator)") + { + // Declares 32-byte document but only contains the size field followed by an immediate terminator. + std::vector const v = + { + 0x20, 0x00, 0x00, 0x00, + 0x00 + }; + json _; + CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 5: syntax error while parsing BSON document: BSON document terminator did not land at declared document size", json::parse_error&); + CHECK(json::from_bson(v, true, false).is_discarded()); + } + + SECTION("array declared size must match consumed bytes") + { + // Outer object contains an array "a" that declares 5 bytes (empty) but + // actually contains an int32 element before its terminator. + std::vector const v = + { + 0x14, 0x00, 0x00, 0x00, // object size = 20 + 0x04, 'a', 0x00, // key "a", array type + 0x05, 0x00, 0x00, 0x00, // array declared size = 5 (empty) + 0x10, '0', 0x00, 0x01, 0x00, 0x00, 0x00, // extra int32 element "0" = 1 + 0x00, // array terminator + 0x00 // object terminator + }; + json _; + CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 19: syntax error while parsing BSON array: BSON array terminator did not land at declared array size", json::parse_error&); + CHECK(json::from_bson(v, true, false).is_discarded()); + } + + SECTION("BSON string must end with 0x00") + { + // Length-prefixed string whose terminator byte is 'X' (0x58), not 0x00. + std::vector const v = + { + 0x0F, 0x00, 0x00, 0x00, + 0x02, 's', 0x00, + 0x02, 0x00, 0x00, 0x00, + 'A', 'X', + 0x00 + }; + json _; + CHECK_THROWS_WITH_AS(_ = json::from_bson(v), "[json.exception.parse_error.112] parse error at byte 13: syntax error while parsing BSON string: BSON string is not null-terminated", json::parse_error&); + CHECK(json::from_bson(v, true, false).is_discarded()); + } +} diff --git a/tests/src/unit-serialization.cpp b/tests/src/unit-serialization.cpp index c0f2a8e046..f55ed84704 100644 --- a/tests/src/unit-serialization.cpp +++ b/tests/src/unit-serialization.cpp @@ -311,14 +311,16 @@ TEST_CASE("dump for basic_json with long double number_float_t") SECTION("round-trip dump/parse") { constexpr std::array values = - {{ - 0.0L, -0.0L, 1.0L, -1.0L, - 0.5L, -0.5L, 1.5L, -2.25L, - 1.23e45L, 1.23e-45L, - (std::numeric_limits::min)(), - std::numeric_limits::lowest(), - (std::numeric_limits::max)() - }}; + { + { + 0.0L, -0.0L, 1.0L, -1.0L, + 0.5L, -0.5L, 1.5L, -2.25L, + 1.23e45L, 1.23e-45L, + (std::numeric_limits::min)(), + std::numeric_limits::lowest(), + (std::numeric_limits::max)() + } + }; for (long double v : values) {