diff --git a/include/nlohmann/detail/input/binary_reader.hpp b/include/nlohmann/detail/input/binary_reader.hpp index c37c603d3..3f82a5703 100644 --- a/include/nlohmann/detail/input/binary_reader.hpp +++ b/include/nlohmann/detail/input/binary_reader.hpp @@ -704,13 +704,15 @@ class binary_reader case 0x9A: // array (four-byte uint32_t for n follow) { std::uint32_t len{}; - return get_number(input_format_t::cbor, len) && get_cbor_array(conditional_static_cast(len), tag_handler); + std::size_t size{}; + return get_number(input_format_t::cbor, len) && get_cbor_container_size(len, size, "array") && get_cbor_array(size, tag_handler); } case 0x9B: // array (eight-byte uint64_t for n follow) { std::uint64_t len{}; - return get_number(input_format_t::cbor, len) && get_cbor_array(conditional_static_cast(len), tag_handler); + std::size_t size{}; + return get_number(input_format_t::cbor, len) && get_cbor_container_size(len, size, "array") && get_cbor_array(size, tag_handler); } case 0x9F: // array (indefinite length) @@ -758,13 +760,15 @@ class binary_reader case 0xBA: // map (four-byte uint32_t for n follow) { std::uint32_t len{}; - return get_number(input_format_t::cbor, len) && get_cbor_object(conditional_static_cast(len), tag_handler); + std::size_t size{}; + return get_number(input_format_t::cbor, len) && get_cbor_container_size(len, size, "map") && get_cbor_object(size, tag_handler); } case 0xBB: // map (eight-byte uint64_t for n follow) { std::uint64_t len{}; - return get_number(input_format_t::cbor, len) && get_cbor_object(conditional_static_cast(len), tag_handler); + std::size_t size{}; + return get_number(input_format_t::cbor, len) && get_cbor_container_size(len, size, "map") && get_cbor_object(size, tag_handler); } case 0xBF: // map (indefinite length) @@ -1155,6 +1159,31 @@ class binary_reader } } + /*! + @brief narrow a definite CBOR 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- + length container and would otherwise make the length read as indefinite. + Both cases exceed any container's max_size(), so no representable input + is affected. + + @param[in] len the declared length + @param[out] result the length narrowed to std::size_t + @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) + { + if (JSON_HEDLEY_UNLIKELY(!value_in_range_of(len) || len == detail::unknown_size())) + { + return sax->parse_error(chars_read, get_token_string(), out_of_range::create(408, + exception_message(input_format_t::cbor, concat("excessive ", context, " size"), "size"), nullptr)); + } + result = conditional_static_cast(len); + return true; + } + /*! @param[in] len the length of the array or detail::unknown_size() for an array of indefinite size diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 6e4d66258..17fea3abf 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -11253,13 +11253,15 @@ class binary_reader case 0x9A: // array (four-byte uint32_t for n follow) { std::uint32_t len{}; - return get_number(input_format_t::cbor, len) && get_cbor_array(conditional_static_cast(len), tag_handler); + std::size_t size{}; + return get_number(input_format_t::cbor, len) && get_cbor_container_size(len, size, "array") && get_cbor_array(size, tag_handler); } case 0x9B: // array (eight-byte uint64_t for n follow) { std::uint64_t len{}; - return get_number(input_format_t::cbor, len) && get_cbor_array(conditional_static_cast(len), tag_handler); + std::size_t size{}; + return get_number(input_format_t::cbor, len) && get_cbor_container_size(len, size, "array") && get_cbor_array(size, tag_handler); } case 0x9F: // array (indefinite length) @@ -11307,13 +11309,15 @@ class binary_reader case 0xBA: // map (four-byte uint32_t for n follow) { std::uint32_t len{}; - return get_number(input_format_t::cbor, len) && get_cbor_object(conditional_static_cast(len), tag_handler); + std::size_t size{}; + return get_number(input_format_t::cbor, len) && get_cbor_container_size(len, size, "map") && get_cbor_object(size, tag_handler); } case 0xBB: // map (eight-byte uint64_t for n follow) { std::uint64_t len{}; - return get_number(input_format_t::cbor, len) && get_cbor_object(conditional_static_cast(len), tag_handler); + std::size_t size{}; + return get_number(input_format_t::cbor, len) && get_cbor_container_size(len, size, "map") && get_cbor_object(size, tag_handler); } case 0xBF: // map (indefinite length) @@ -11704,6 +11708,31 @@ class binary_reader } } + /*! + @brief narrow a definite CBOR 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- + length container and would otherwise make the length read as indefinite. + Both cases exceed any container's max_size(), so no representable input + is affected. + + @param[in] len the declared length + @param[out] result the length narrowed to std::size_t + @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) + { + if (JSON_HEDLEY_UNLIKELY(!value_in_range_of(len) || len == detail::unknown_size())) + { + return sax->parse_error(chars_read, get_token_string(), out_of_range::create(408, + exception_message(input_format_t::cbor, concat("excessive ", context, " size"), "size"), nullptr)); + } + result = conditional_static_cast(len); + return true; + } + /*! @param[in] len the length of the array or detail::unknown_size() for an array of indefinite size diff --git a/tests/src/unit-32bit.cpp b/tests/src/unit-32bit.cpp index e160304e8..aead04205 100644 --- a/tests/src/unit-32bit.cpp +++ b/tests/src/unit-32bit.cpp @@ -132,3 +132,37 @@ TEST_CASE("BJData") } } } + +TEST_CASE("CBOR") +{ + SECTION("parse errors") + { + SECTION("array/map size larger than std::size_t") + { + // declared lengths do not fit in a 32-bit std::size_t and must not be truncated + std::vector const varr = {0x9B, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05}; + std::vector const vmap = {0xBB, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05}; + + json _; + CHECK_THROWS_WITH_AS(_ = json::from_cbor(varr), "[json.exception.out_of_range.408] syntax error while parsing CBOR size: excessive array size", json::out_of_range&); + CHECK(json::from_cbor(varr, true, false).is_discarded()); + + CHECK_THROWS_WITH_AS(_ = json::from_cbor(vmap), "[json.exception.out_of_range.408] syntax error while parsing CBOR size: excessive map size", json::out_of_range&); + CHECK(json::from_cbor(vmap, true, false).is_discarded()); + } + + SECTION("array/map size equal to the indefinite-length sentinel") + { + // on 32-bit platforms a four-byte length of 0xFFFFFFFF aliases unknown_size() + std::vector const varr = {0x9A, 0xFF, 0xFF, 0xFF, 0xFF}; + std::vector const vmap = {0xBA, 0xFF, 0xFF, 0xFF, 0xFF}; + + json _; + CHECK_THROWS_WITH_AS(_ = json::from_cbor(varr), "[json.exception.out_of_range.408] syntax error while parsing CBOR size: excessive array size", json::out_of_range&); + CHECK(json::from_cbor(varr, true, false).is_discarded()); + + CHECK_THROWS_WITH_AS(_ = json::from_cbor(vmap), "[json.exception.out_of_range.408] syntax error while parsing CBOR size: excessive map size", json::out_of_range&); + CHECK(json::from_cbor(vmap, true, false).is_discarded()); + } + } +} diff --git a/tests/src/unit-cbor.cpp b/tests/src/unit-cbor.cpp index 42faa9809..1b4b95dbf 100644 --- a/tests/src/unit-cbor.cpp +++ b/tests/src/unit-cbor.cpp @@ -1999,6 +1999,42 @@ TEST_CASE("CBOR regressions") } #endif +TEST_CASE("CBOR definite length equal to the indefinite-length sentinel") +{ + // A definite-length array or map whose declared element count equals the + // reserved unknown_size() sentinel (SIZE_MAX) must be rejected. Otherwise + // it is read as an indefinite-length container and the following bytes are + // silently accepted instead of the (impossible) count being reported. + json _; + + SECTION("array") + { + // 0x9B: array with eight-byte length; length = 0xFFFFFFFFFFFFFFFF + const std::vector input = {0x9B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x02, 0xFF}; + CHECK_THROWS_WITH_AS(_ = json::from_cbor(input), "[json.exception.out_of_range.408] syntax error while parsing CBOR size: excessive array size", json::out_of_range&); + } + + SECTION("map") + { + // 0xBB: map with eight-byte length; length = 0xFFFFFFFFFFFFFFFF + const std::vector input = {0xBB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x61, 0x61, 0x01, 0xFF}; + CHECK_THROWS_WITH_AS(_ = json::from_cbor(input), "[json.exception.out_of_range.408] syntax error while parsing CBOR size: excessive map size", json::out_of_range&); + } + + SECTION("indefinite-length containers are unaffected") + { + CHECK(json::from_cbor(std::vector({0x9F, 0x01, 0x02, 0xFF})) == json({1, 2})); + CHECK(json::from_cbor(std::vector({0xBF, 0x61, 0x61, 0x01, 0xFF})) == json({{"a", 1}})); + } + + SECTION("ordinary four-byte length containers are unaffected") + { + // 0x9A/0xBA carry a four-byte length; a normal count still parses + CHECK(json::from_cbor(std::vector({0x9A, 0x00, 0x00, 0x00, 0x02, 0x01, 0x02})) == json({1, 2})); + CHECK(json::from_cbor(std::vector({0xBA, 0x00, 0x00, 0x00, 0x01, 0x61, 0x61, 0x01})) == json({{"a", 1}})); + } +} + TEST_CASE("CBOR roundtrips" * doctest::skip()) { SECTION("input from flynn")