From efc437ef6e25d0e5617c4b6385a095cb383a5a2b Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 4 Nov 2026 17:13:16 +0100 Subject: [PATCH] UBJSON/BJData/MessagePack reader: reject a container count equal to SIZE_MAX (#6002) * UBJSON/BJData reader: reject a container count equal to SIZE_MAX The reader marks a container whose count is not known by npos, which is SIZE_MAX. A count of exactly that value was stored unchanged and then read as "no count", so the BJData input [ # M ff ff ff ff ff ff ff ff i 01 ] parsed as [1] instead of throwing out_of_range.408 as a count of 2^64-2 does. Where size_t is 32 bits, the same holds for 'm' and 'L' counts of 2^32-1, in UBJSON too. The counts of 'm', 'M' and the signed types now go through store_ubjson_count(), which rejects npos with "integer value overflow" (the message a count beyond size_t already gets), as the CBOR reader does in get_cbor_container_size(). An ND-array dimension of 2^64-1 gets the same message now, where it was "excessive ndarray size caused overflow" (the product check) on 64-bit platforms. Signed-off-by: Niels Lohmann * MessagePack reader: reject an array/map 32 length equal to SIZE_MAX On a platform with a 32-bit std::size_t, an array 32 or map 32 length of 0xFFFFFFFF equals unknown_size(), so start_array()/start_object() were told the size was unknown and skipped the max_size() check. Route both through the CBOR length check, renamed get_container_size(), which already rejects that value. Signed-off-by: Niels Lohmann --------- Signed-off-by: Niels Lohmann --- .../nlohmann/detail/input/binary_reader.hpp | 44 ++++++++++++++----- single_include/nlohmann/json.hpp | 44 ++++++++++++++----- tests/src/unit-32bit.cpp | 20 +++++++++ tests/src/unit-bjdata.cpp | 28 ++++++++++-- tests/src/unit-regression2.cpp | 2 +- tests/src/unit-ubjson.cpp | 12 +++++ 6 files changed, 121 insertions(+), 29 deletions(-) diff --git a/include/nlohmann/detail/input/binary_reader.hpp b/include/nlohmann/detail/input/binary_reader.hpp index 8edc53ae9..b793ae097 100644 --- a/include/nlohmann/detail/input/binary_reader.hpp +++ b/include/nlohmann/detail/input/binary_reader.hpp @@ -1024,7 +1024,7 @@ class binary_reader { std::uint64_t len{}; std::size_t size{}; - return get_cbor_argument(len) && get_cbor_container_size(len, size, "array") && enter_cbor_array(size); + return get_cbor_argument(len) && get_container_size(len, size, "array") && enter_cbor_array(size); } case 0x9F: // array (indefinite length) @@ -1064,7 +1064,7 @@ class binary_reader { std::uint64_t len{}; std::size_t size{}; - return get_cbor_argument(len) && get_cbor_container_size(len, size, "map") && enter_object(size); + return get_cbor_argument(len) && get_container_size(len, size, "map") && enter_object(size); } case 0xBF: // map (indefinite length) @@ -1700,7 +1700,7 @@ class binary_reader } /*! - @brief narrow a definite CBOR array/map length to std::size_t + @brief narrow a definite CBOR or MessagePack array/map length to std::size_t A definite length is rejected if it does not fit in std::size_t or if it equals detail::unknown_size(), which is reserved to mark an indefinite- @@ -1713,7 +1713,7 @@ class binary_reader @param[in] context "array" or "map", for the error message @return whether the length is usable */ - bool get_cbor_container_size(const std::uint64_t len, std::size_t& result, const char* context) + bool get_container_size(const std::uint64_t len, std::size_t& result, const char* context) { if (JSON_HEDLEY_UNLIKELY(!value_in_range_of(len) || len == detail::unknown_size())) { @@ -2309,7 +2309,8 @@ class binary_reader case 0xDD: // array 32 { std::uint32_t len{}; - return get_number(len) && enter_msgpack_array(conditional_static_cast(len)); + std::size_t size{}; + return get_number(len) && get_container_size(len, size, "array") && enter_msgpack_array(size); } case 0xDE: // map 16 @@ -2321,7 +2322,8 @@ class binary_reader case 0xDF: // map 32 { std::uint32_t len{}; - return get_number(len) && enter_object(conditional_static_cast(len)); + std::size_t size{}; + return get_number(len) && get_container_size(len, size, "map") && enter_object(size); } // negative fixint @@ -3131,6 +3133,27 @@ class binary_reader return true; } + /*! + @brief store the count of an optimized container (or the length of a string) + + A count equal to npos is rejected: npos marks a container whose count is + not known, so a count of that value would be read as "no count" (compare + get_container_size()). No container can hold that many elements. + + @param[in] count the count read from the input + @param[out] result set to @a count unless it is rejected + @return whether @a count is usable + */ + bool store_ubjson_count(const std::size_t count, std::size_t& result) + { + if (JSON_HEDLEY_UNLIKELY(count == npos)) + { + return out_of_range_error(exception_id::container_too_large, "integer value overflow", "size"); + } + result = count; + return true; + } + /*! @brief read a UBJSON/BJData optimized-container count of a signed marker type ('i', 'I', 'l', 'L') and narrow it to std::size_t @@ -3161,8 +3184,7 @@ class binary_reader { return out_of_range_error(exception_id::container_too_large, "integer value overflow", "size"); } - result = static_cast(number); // NOLINT(bugprone-signed-char-misuse,cert-str34-c): number is not a char - return true; + return store_ubjson_count(static_cast(number), result); // NOLINT(bugprone-signed-char-misuse,cert-str34-c): number is not a char } /*! @@ -3239,8 +3261,7 @@ class binary_reader { return false; } - result = conditional_static_cast(number); - return true; + return store_ubjson_count(conditional_static_cast(number), result); } case 'M': @@ -3258,8 +3279,7 @@ class binary_reader { return out_of_range_error(exception_id::container_too_large, "integer value overflow", "size"); } - result = detail::conditional_static_cast(number); - return true; + return store_ubjson_count(detail::conditional_static_cast(number), result); } case '[': diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 952cea7b6..48c99397b 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -19039,7 +19039,7 @@ class binary_reader { std::uint64_t len{}; std::size_t size{}; - return get_cbor_argument(len) && get_cbor_container_size(len, size, "array") && enter_cbor_array(size); + return get_cbor_argument(len) && get_container_size(len, size, "array") && enter_cbor_array(size); } case 0x9F: // array (indefinite length) @@ -19079,7 +19079,7 @@ class binary_reader { std::uint64_t len{}; std::size_t size{}; - return get_cbor_argument(len) && get_cbor_container_size(len, size, "map") && enter_object(size); + return get_cbor_argument(len) && get_container_size(len, size, "map") && enter_object(size); } case 0xBF: // map (indefinite length) @@ -19715,7 +19715,7 @@ class binary_reader } /*! - @brief narrow a definite CBOR array/map length to std::size_t + @brief narrow a definite CBOR or MessagePack array/map length to std::size_t A definite length is rejected if it does not fit in std::size_t or if it equals detail::unknown_size(), which is reserved to mark an indefinite- @@ -19728,7 +19728,7 @@ class binary_reader @param[in] context "array" or "map", for the error message @return whether the length is usable */ - bool get_cbor_container_size(const std::uint64_t len, std::size_t& result, const char* context) + bool get_container_size(const std::uint64_t len, std::size_t& result, const char* context) { if (JSON_HEDLEY_UNLIKELY(!value_in_range_of(len) || len == detail::unknown_size())) { @@ -20324,7 +20324,8 @@ class binary_reader case 0xDD: // array 32 { std::uint32_t len{}; - return get_number(len) && enter_msgpack_array(conditional_static_cast(len)); + std::size_t size{}; + return get_number(len) && get_container_size(len, size, "array") && enter_msgpack_array(size); } case 0xDE: // map 16 @@ -20336,7 +20337,8 @@ class binary_reader case 0xDF: // map 32 { std::uint32_t len{}; - return get_number(len) && enter_object(conditional_static_cast(len)); + std::size_t size{}; + return get_number(len) && get_container_size(len, size, "map") && enter_object(size); } // negative fixint @@ -21146,6 +21148,27 @@ class binary_reader return true; } + /*! + @brief store the count of an optimized container (or the length of a string) + + A count equal to npos is rejected: npos marks a container whose count is + not known, so a count of that value would be read as "no count" (compare + get_container_size()). No container can hold that many elements. + + @param[in] count the count read from the input + @param[out] result set to @a count unless it is rejected + @return whether @a count is usable + */ + bool store_ubjson_count(const std::size_t count, std::size_t& result) + { + if (JSON_HEDLEY_UNLIKELY(count == npos)) + { + return out_of_range_error(exception_id::container_too_large, "integer value overflow", "size"); + } + result = count; + return true; + } + /*! @brief read a UBJSON/BJData optimized-container count of a signed marker type ('i', 'I', 'l', 'L') and narrow it to std::size_t @@ -21176,8 +21199,7 @@ class binary_reader { return out_of_range_error(exception_id::container_too_large, "integer value overflow", "size"); } - result = static_cast(number); // NOLINT(bugprone-signed-char-misuse,cert-str34-c): number is not a char - return true; + return store_ubjson_count(static_cast(number), result); // NOLINT(bugprone-signed-char-misuse,cert-str34-c): number is not a char } /*! @@ -21254,8 +21276,7 @@ class binary_reader { return false; } - result = conditional_static_cast(number); - return true; + return store_ubjson_count(conditional_static_cast(number), result); } case 'M': @@ -21273,8 +21294,7 @@ class binary_reader { return out_of_range_error(exception_id::container_too_large, "integer value overflow", "size"); } - result = detail::conditional_static_cast(number); - return true; + return store_ubjson_count(detail::conditional_static_cast(number), result); } case '[': diff --git a/tests/src/unit-32bit.cpp b/tests/src/unit-32bit.cpp index 97549c999..e4e2dea6d 100644 --- a/tests/src/unit-32bit.cpp +++ b/tests/src/unit-32bit.cpp @@ -97,3 +97,23 @@ TEST_CASE("CBOR") } } } + +TEST_CASE("MessagePack") +{ + SECTION("parse errors") + { + SECTION("array/map size equal to the unknown-size sentinel") + { + // on 32-bit platforms an array 32/map 32 length of 0xFFFFFFFF aliases unknown_size() + std::vector const varr = {0xDD, 0xFF, 0xFF, 0xFF, 0xFF}; + std::vector const vmap = {0xDF, 0xFF, 0xFF, 0xFF, 0xFF}; + + json _; + CHECK_THROWS_WITH_AS(_ = json::from_msgpack(varr), "[json.exception.out_of_range.408] syntax error while parsing MessagePack size: excessive array size", json::out_of_range&); + CHECK(json::from_msgpack(varr, true, false).is_discarded()); + + CHECK_THROWS_WITH_AS(_ = json::from_msgpack(vmap), "[json.exception.out_of_range.408] syntax error while parsing MessagePack size: excessive map size", json::out_of_range&); + CHECK(json::from_msgpack(vmap, true, false).is_discarded()); + } + } +} diff --git a/tests/src/unit-bjdata.cpp b/tests/src/unit-bjdata.cpp index ec7faa6cd..ecc79959d 100644 --- a/tests/src/unit-bjdata.cpp +++ b/tests/src/unit-bjdata.cpp @@ -3237,14 +3237,34 @@ TEST_CASE("BJData") #endif CHECK(json::from_bjdata(vM, true, false).is_discarded()); -#if SIZE_MAX != 0xffffffff - CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vMX), "[json.exception.out_of_range.408] syntax error while parsing BJData size: excessive ndarray size caused overflow", json::out_of_range&); -#else + // the first dimension is 2^64-1, which is rejected as a count (see below) CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vMX), "[json.exception.out_of_range.408] syntax error while parsing BJData size: integer value overflow", json::out_of_range&); -#endif CHECK(json::from_bjdata(vMX, true, false).is_discarded()); } + SECTION("optimized array: count equal to SIZE_MAX") + { + // the reader marks a container without a count by SIZE_MAX, so + // a count of that value must not be read as "no count" + // (it used to parse as [1]) + json _; +#if SIZE_MAX != 0xffffffff + std::vector const vM = {'[', '#', 'M', 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 'i', 0x01, ']'}; + CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vM), "[json.exception.out_of_range.408] syntax error while parsing BJData size: integer value overflow", json::out_of_range&); + CHECK(json::from_bjdata(vM, true, false).is_discarded()); + // one less is an ordinary excessive count + std::vector const vM1 = {'[', '#', 'M', 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 'i', 0x01, ']'}; + CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vM1), "[json.exception.out_of_range.408] excessive array size: 18446744073709551614", json::out_of_range&); +#else + std::vector const vm = {'[', '#', 'm', 0xFF, 0xFF, 0xFF, 0xFF, 'i', 0x01, ']'}; + CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vm), "[json.exception.out_of_range.408] syntax error while parsing BJData size: integer value overflow", json::out_of_range&); + CHECK(json::from_bjdata(vm, true, false).is_discarded()); + std::vector const vL = {'[', '#', 'L', 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 'i', 0x01, ']'}; + CHECK_THROWS_WITH_AS(_ = json::from_bjdata(vL), "[json.exception.out_of_range.408] syntax error while parsing BJData size: integer value overflow", json::out_of_range&); + CHECK(json::from_bjdata(vL, true, false).is_discarded()); +#endif + } + SECTION("optimized array: integer value overflow") { #if SIZE_MAX == 0xffffffff diff --git a/tests/src/unit-regression2.cpp b/tests/src/unit-regression2.cpp index 08d342d0a..f7bd557e8 100644 --- a/tests/src/unit-regression2.cpp +++ b/tests/src/unit-regression2.cpp @@ -1009,7 +1009,7 @@ TEST_CASE("regression test - excessive binary container size honors allow_except // allow_exceptions=true (the default) must still throw exactly as before. // The exact message text is not checked here: on platforms where // std::size_t is 32-bit, the CBOR reader's own length-narrowing check - // (get_cbor_container_size(), unrelated to this fix) intercepts a + // (get_container_size(), unrelated to this fix) intercepts a // declared length of 2^63 before it ever reaches the check this test // targets, with different (but equally valid, and already correct) // wording -- see unit-cbor.cpp for coverage of that message. diff --git a/tests/src/unit-ubjson.cpp b/tests/src/unit-ubjson.cpp index 4bc7f7ac0..b4fdc0997 100644 --- a/tests/src/unit-ubjson.cpp +++ b/tests/src/unit-ubjson.cpp @@ -2385,6 +2385,18 @@ TEST_CASE("UBJSON optimized arrays of a valueless type are bounded") } } + SECTION("a count equal to SIZE_MAX is rejected") + { + // the reader marks a container without a count by SIZE_MAX, so a + // count of that value must not be read as "no count"; UBJSON's + // largest count type (int64) can only reach it where size_t is 32 bits +#if SIZE_MAX == 0xffffffff + const std::vector input = {'[', '#', 'L', 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 'i', 0x01, ']'}; + CHECK_THROWS_WITH_AS(_ = json::from_ubjson(input), "[json.exception.out_of_range.408] syntax error while parsing UBJSON size: integer value overflow", json::out_of_range&); + CHECK(json::from_ubjson(input, true, false).is_discarded()); +#endif + } + SECTION("ordinary counts are unaffected") { CHECK(json::from_ubjson(std::vector({'[', '$', 'Z', '#', 'i', 3})) == json({nullptr, nullptr, nullptr}));