From 530ab84ed6f12cbeaeed20c2798338f79674c112 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Tue, 19 May 2026 14:18:55 +0200 Subject: [PATCH 1/3] :bug: fix BSON conformance issue Signed-off-by: Niels Lohmann --- .../nlohmann/detail/input/binary_reader.hpp | 60 ++++++++++++++++++- single_include/nlohmann/json.hpp | 60 ++++++++++++++++++- tests/src/unit-bson.cpp | 39 ++++++++++++ tests/src/unit-serialization.cpp | 18 +++--- 4 files changed, 167 insertions(+), 10 deletions(-) diff --git a/include/nlohmann/detail/input/binary_reader.hpp b/include/nlohmann/detail/input/binary_reader.hpp index 033cfebd78..2a89d2b66d 100644 --- a/include/nlohmann/detail/input/binary_reader.hpp +++ b/include/nlohmann/detail/input/binary_reader.hpp @@ -172,6 +172,19 @@ class binary_reader std::int32_t document_size{}; get_number(input_format_t::bson, document_size); + 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)); + } + + // chars_read now points just past the 4-byte size field; + // the document started 4 bytes earlier and must end at start + document_size. + const std::size_t expected_end = chars_read + static_cast(document_size) - 4; + if (JSON_HEDLEY_UNLIKELY(!sax->start_object(detail::unknown_size()))) { return false; @@ -182,6 +195,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 +253,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; } /*! @@ -400,6 +436,19 @@ class binary_reader std::int32_t document_size{}; get_number(input_format_t::bson, document_size); + 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)); + } + + // chars_read now points just past the 4-byte size field; + // the document started 4 bytes earlier and must end at start + document_size. + const std::size_t expected_end = chars_read + static_cast(document_size) - 4; + if (JSON_HEDLEY_UNLIKELY(!sax->start_array(detail::unknown_size()))) { return false; @@ -410,6 +459,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_array(); } diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 2e16ad5b9e..406df31e51 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -10314,6 +10314,19 @@ class binary_reader std::int32_t document_size{}; get_number(input_format_t::bson, document_size); + 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)); + } + + // chars_read now points just past the 4-byte size field; + // the document started 4 bytes earlier and must end at start + document_size. + const std::size_t expected_end = chars_read + static_cast(document_size) - 4; + if (JSON_HEDLEY_UNLIKELY(!sax->start_object(detail::unknown_size()))) { return false; @@ -10324,6 +10337,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 +10395,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; } /*! @@ -10542,6 +10578,19 @@ class binary_reader std::int32_t document_size{}; get_number(input_format_t::bson, document_size); + 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)); + } + + // chars_read now points just past the 4-byte size field; + // the document started 4 bytes earlier and must end at start + document_size. + const std::size_t expected_end = chars_read + static_cast(document_size) - 4; + if (JSON_HEDLEY_UNLIKELY(!sax->start_array(detail::unknown_size()))) { return false; @@ -10552,6 +10601,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_array(); } diff --git a/tests/src/unit-bson.cpp b/tests/src/unit-bson.cpp index eed70352e8..b5cd0041f6 100644 --- a/tests/src/unit-bson.cpp +++ b/tests/src/unit-bson.cpp @@ -1294,3 +1294,42 @@ 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&); + } + + 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&); + } + + 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&); + } +} \ No newline at end of file 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) { From c78d9dc386c5fcb48cdabb643e7799be094fab71 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Tue, 19 May 2026 14:32:01 +0200 Subject: [PATCH 2/3] :bug: fix BSON conformance issue Signed-off-by: Niels Lohmann --- .../nlohmann/detail/input/binary_reader.hpp | 24 ++++++++----- single_include/nlohmann/json.hpp | 24 ++++++++----- tests/src/unit-bson.cpp | 36 ++++++++++++++++++- 3 files changed, 67 insertions(+), 17 deletions(-) diff --git a/include/nlohmann/detail/input/binary_reader.hpp b/include/nlohmann/detail/input/binary_reader.hpp index 2a89d2b66d..f4df800c22 100644 --- a/include/nlohmann/detail/input/binary_reader.hpp +++ b/include/nlohmann/detail/input/binary_reader.hpp @@ -170,7 +170,10 @@ 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)) { @@ -181,9 +184,10 @@ class binary_reader "document size"), nullptr)); } - // chars_read now points just past the 4-byte size field; - // the document started 4 bytes earlier and must end at start + document_size. - const std::size_t expected_end = chars_read + static_cast(document_size) - 4; + // 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()))) { @@ -434,7 +438,10 @@ 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)) { @@ -445,9 +452,10 @@ class binary_reader "document size"), nullptr)); } - // chars_read now points just past the 4-byte size field; - // the document started 4 bytes earlier and must end at start + document_size. - const std::size_t expected_end = chars_read + static_cast(document_size) - 4; + // 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()))) { diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 406df31e51..3d269799e4 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -10312,7 +10312,10 @@ 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)) { @@ -10323,9 +10326,10 @@ class binary_reader "document size"), nullptr)); } - // chars_read now points just past the 4-byte size field; - // the document started 4 bytes earlier and must end at start + document_size. - const std::size_t expected_end = chars_read + static_cast(document_size) - 4; + // 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()))) { @@ -10576,7 +10580,10 @@ 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)) { @@ -10587,9 +10594,10 @@ class binary_reader "document size"), nullptr)); } - // chars_read now points just past the 4-byte size field; - // the document started 4 bytes earlier and must end at start + document_size. - const std::size_t expected_end = chars_read + static_cast(document_size) - 4; + // 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()))) { diff --git a/tests/src/unit-bson.cpp b/tests/src/unit-bson.cpp index b5cd0041f6..53c308a23c 100644 --- a/tests/src/unit-bson.cpp +++ b/tests/src/unit-bson.cpp @@ -1302,6 +1302,7 @@ TEST_CASE("Invalid document size handling") 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)") @@ -1316,6 +1317,38 @@ TEST_CASE("Invalid document size handling") }; 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 document: BSON document terminator did not land at declared document size", json::parse_error&); + CHECK(json::from_bson(v, true, false).is_discarded()); } SECTION("BSON string must end with 0x00") @@ -1331,5 +1364,6 @@ TEST_CASE("Invalid document size handling") }; 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()); } -} \ No newline at end of file +} From 42f4df11597ba16f62f00a4826796090ea17e4a9 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Tue, 19 May 2026 15:13:08 +0200 Subject: [PATCH 3/3] :bug: fix BSON conformance issue Signed-off-by: Niels Lohmann --- include/nlohmann/detail/input/binary_reader.hpp | 4 ++-- single_include/nlohmann/json.hpp | 4 ++-- tests/src/unit-bson.cpp | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/include/nlohmann/detail/input/binary_reader.hpp b/include/nlohmann/detail/input/binary_reader.hpp index f4df800c22..63dfffa9ca 100644 --- a/include/nlohmann/detail/input/binary_reader.hpp +++ b/include/nlohmann/detail/input/binary_reader.hpp @@ -472,8 +472,8 @@ class binary_reader 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)); + "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 3d269799e4..411bb01efe 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -10614,8 +10614,8 @@ class binary_reader 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)); + "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 53c308a23c..e801250fa3 100644 --- a/tests/src/unit-bson.cpp +++ b/tests/src/unit-bson.cpp @@ -1347,7 +1347,7 @@ TEST_CASE("Invalid document size handling") 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 document: BSON document terminator did not land at declared document size", json::parse_error&); + 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()); }