From 457bc283ffb3dc4faffab25734af5661506efff0 Mon Sep 17 00:00:00 2001 From: Ville Vesilehto Date: Mon, 12 Jan 2026 11:17:47 +0200 Subject: [PATCH] fix(cbor): reject overflowing negative integers (#5039) * fix(cbor): reject negative ints overflowing int64 CBOR encodes negative integers as "-1 - n" where n is uint64_t. When n > INT64_MAX, casting to int64_t caused undefined behavior and silent data corruption. Large negative values were incorrectly parsed as positive integers (e.g., -9223372036854775809 became 9223372036854775807). Add bounds check for to reject values that exceed int64_t representable range, returning parse_error instead of silently corrupting data. Added regression test cases to verify. Signed-off-by: Ville Vesilehto * chore: clarify tests Add test for "n=0" case (result=-1) to cover the smallest magnitude boundary. Update comments to explain CBOR 0x3B encoding and why "result=0" is not possible. Clarify that n is an unsigned integer in the formula "result = -1 - n" to help understanding the tests. Signed-off-by: Ville Vesilehto * fix(cbor): extend overflow checks for other types Extend negative integer overflow detection to all CBOR negative integer cases (0x38, 0x39, 0x3A) for consistency with the existing 0x3B check. Signed-off-by: Ville Vesilehto --------- Signed-off-by: Ville Vesilehto --- .../nlohmann/detail/input/binary_reader.hpp | 40 +++++----- single_include/nlohmann/json.hpp | 40 +++++----- tests/src/unit-cbor.cpp | 76 +++++++++++++++++++ 3 files changed, 122 insertions(+), 34 deletions(-) diff --git a/include/nlohmann/detail/input/binary_reader.hpp b/include/nlohmann/detail/input/binary_reader.hpp index c6453f74c..033cfebd7 100644 --- a/include/nlohmann/detail/input/binary_reader.hpp +++ b/include/nlohmann/detail/input/binary_reader.hpp @@ -425,6 +425,25 @@ class binary_reader @return whether a valid CBOR value was passed to the SAX parser */ + + template + bool get_cbor_negative_integer() + { + NumberType number{}; + if (JSON_HEDLEY_UNLIKELY(!get_number(input_format_t::cbor, number))) + { + return false; + } + const auto max_val = static_cast((std::numeric_limits::max)()); + if (number > max_val) + { + return sax->parse_error(chars_read, get_token_string(), + parse_error::create(112, chars_read, + exception_message(input_format_t::cbor, "negative integer overflow", "value"), nullptr)); + } + return sax->number_integer(static_cast(-1) - static_cast(number)); + } + bool parse_cbor_internal(const bool get_char, const cbor_tag_handler_t tag_handler) { @@ -513,29 +532,16 @@ class binary_reader return sax->number_integer(static_cast(0x20 - 1 - current)); case 0x38: // Negative integer (one-byte uint8_t follows) - { - std::uint8_t number{}; - return get_number(input_format_t::cbor, number) && sax->number_integer(static_cast(-1) - number); - } + return get_cbor_negative_integer(); case 0x39: // Negative integer -1-n (two-byte uint16_t follows) - { - std::uint16_t number{}; - return get_number(input_format_t::cbor, number) && sax->number_integer(static_cast(-1) - number); - } + return get_cbor_negative_integer(); case 0x3A: // Negative integer -1-n (four-byte uint32_t follows) - { - std::uint32_t number{}; - return get_number(input_format_t::cbor, number) && sax->number_integer(static_cast(-1) - number); - } + return get_cbor_negative_integer(); case 0x3B: // Negative integer -1-n (eight-byte uint64_t follows) - { - std::uint64_t number{}; - return get_number(input_format_t::cbor, number) && sax->number_integer(static_cast(-1) - - static_cast(number)); - } + return get_cbor_negative_integer(); // Binary data (0x00..0x17 bytes follow) case 0x40: diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 27314f4b3..81b22b9bd 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -10266,6 +10266,25 @@ class binary_reader @return whether a valid CBOR value was passed to the SAX parser */ + + template + bool get_cbor_negative_integer() + { + NumberType number{}; + if (JSON_HEDLEY_UNLIKELY(!get_number(input_format_t::cbor, number))) + { + return false; + } + const auto max_val = static_cast((std::numeric_limits::max)()); + if (number > max_val) + { + return sax->parse_error(chars_read, get_token_string(), + parse_error::create(112, chars_read, + exception_message(input_format_t::cbor, "negative integer overflow", "value"), nullptr)); + } + return sax->number_integer(static_cast(-1) - static_cast(number)); + } + bool parse_cbor_internal(const bool get_char, const cbor_tag_handler_t tag_handler) { @@ -10354,29 +10373,16 @@ class binary_reader return sax->number_integer(static_cast(0x20 - 1 - current)); case 0x38: // Negative integer (one-byte uint8_t follows) - { - std::uint8_t number{}; - return get_number(input_format_t::cbor, number) && sax->number_integer(static_cast(-1) - number); - } + return get_cbor_negative_integer(); case 0x39: // Negative integer -1-n (two-byte uint16_t follows) - { - std::uint16_t number{}; - return get_number(input_format_t::cbor, number) && sax->number_integer(static_cast(-1) - number); - } + return get_cbor_negative_integer(); case 0x3A: // Negative integer -1-n (four-byte uint32_t follows) - { - std::uint32_t number{}; - return get_number(input_format_t::cbor, number) && sax->number_integer(static_cast(-1) - number); - } + return get_cbor_negative_integer(); case 0x3B: // Negative integer -1-n (eight-byte uint64_t follows) - { - std::uint64_t number{}; - return get_number(input_format_t::cbor, number) && sax->number_integer(static_cast(-1) - - static_cast(number)); - } + return get_cbor_negative_integer(); // Binary data (0x00..0x17 bytes follow) case 0x40: diff --git a/tests/src/unit-cbor.cpp b/tests/src/unit-cbor.cpp index 34c259757..5da5b0c7a 100644 --- a/tests/src/unit-cbor.cpp +++ b/tests/src/unit-cbor.cpp @@ -1705,6 +1705,16 @@ TEST_CASE("CBOR") CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector({0x1b, 0x00, 0x00, 0x00, 0x00, 0x00})), "[json.exception.parse_error.110] parse error at byte 7: syntax error while parsing CBOR number: unexpected end of input", json::parse_error&); CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector({0x1b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})), "[json.exception.parse_error.110] parse error at byte 8: syntax error while parsing CBOR number: unexpected end of input", json::parse_error&); CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector({0x1b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})), "[json.exception.parse_error.110] parse error at byte 9: syntax error while parsing CBOR number: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector({0x38})), "[json.exception.parse_error.110] parse error at byte 2: syntax error while parsing CBOR number: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector({0x39})), "[json.exception.parse_error.110] parse error at byte 2: syntax error while parsing CBOR number: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector({0x39, 0x00})), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing CBOR number: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector({0x3a})), "[json.exception.parse_error.110] parse error at byte 2: syntax error while parsing CBOR number: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector({0x3a, 0x00})), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing CBOR number: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector({0x3a, 0x00, 0x00})), "[json.exception.parse_error.110] parse error at byte 4: syntax error while parsing CBOR number: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector({0x3a, 0x00, 0x00, 0x00})), "[json.exception.parse_error.110] parse error at byte 5: syntax error while parsing CBOR number: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector({0x3b})), "[json.exception.parse_error.110] parse error at byte 2: syntax error while parsing CBOR number: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector({0x3b, 0x00})), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing CBOR number: unexpected end of input", json::parse_error&); + CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector({0x3b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})), "[json.exception.parse_error.110] parse error at byte 9: syntax error while parsing CBOR number: unexpected end of input", json::parse_error&); CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector({0x62})), "[json.exception.parse_error.110] parse error at byte 2: syntax error while parsing CBOR string: unexpected end of input", json::parse_error&); CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector({0x62, 0x60})), "[json.exception.parse_error.110] parse error at byte 3: syntax error while parsing CBOR string: unexpected end of input", json::parse_error&); CHECK_THROWS_WITH_AS(_ = json::from_cbor(std::vector({0x7F})), "[json.exception.parse_error.110] parse error at byte 2: syntax error while parsing CBOR string: unexpected end of input", json::parse_error&); @@ -1733,6 +1743,16 @@ TEST_CASE("CBOR") CHECK(json::from_cbor(std::vector({0x1b, 0x00, 0x00, 0x00, 0x00, 0x00}), true, false).is_discarded()); CHECK(json::from_cbor(std::vector({0x1b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}), true, false).is_discarded()); CHECK(json::from_cbor(std::vector({0x1b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}), true, false).is_discarded()); + CHECK(json::from_cbor(std::vector({0x38}), true, false).is_discarded()); + CHECK(json::from_cbor(std::vector({0x39}), true, false).is_discarded()); + CHECK(json::from_cbor(std::vector({0x39, 0x00}), true, false).is_discarded()); + CHECK(json::from_cbor(std::vector({0x3a}), true, false).is_discarded()); + CHECK(json::from_cbor(std::vector({0x3a, 0x00}), true, false).is_discarded()); + CHECK(json::from_cbor(std::vector({0x3a, 0x00, 0x00}), true, false).is_discarded()); + CHECK(json::from_cbor(std::vector({0x3a, 0x00, 0x00, 0x00}), true, false).is_discarded()); + CHECK(json::from_cbor(std::vector({0x3b}), true, false).is_discarded()); + CHECK(json::from_cbor(std::vector({0x3b, 0x00}), true, false).is_discarded()); + CHECK(json::from_cbor(std::vector({0x3b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}), true, false).is_discarded()); CHECK(json::from_cbor(std::vector({0x62}), true, false).is_discarded()); CHECK(json::from_cbor(std::vector({0x62, 0x60}), true, false).is_discarded()); CHECK(json::from_cbor(std::vector({0x7F}), true, false).is_discarded()); @@ -2681,6 +2701,62 @@ TEST_CASE("Tagged values") } } + SECTION("negative integer overflow") + { + // CBOR encodes negative integers as: result = -1 - n + // For type 0x3B, n is an 8-byte uint64_t. Valid range for n with + // the default int64_t is [0, INT64_MAX], producing results in [INT64_MIN, -1]. + // When n > INT64_MAX, the result exceeds int64_t range and is rejected. + + SECTION("n = 0 is valid (result = -1)") + { + // n = 0, result = -1 - 0 = -1 (smallest magnitude negative) + const std::vector input = {0x3B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + const auto result = json::from_cbor(input); + CHECK(result.is_number_integer()); + CHECK(result.get() == -1); + } + + SECTION("n = INT64_MAX is valid (result = INT64_MIN)") + { + // n = INT64_MAX (0x7FFFFFFFFFFFFFFF) + // result = -1 - INT64_MAX = INT64_MIN (-9223372036854775808) + const std::vector input = {0x3B, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + const auto result = json::from_cbor(input); + CHECK(result.is_number_integer()); + CHECK(result.get() == (std::numeric_limits::min)()); + } + + SECTION("n = INT64_MAX + 1 is rejected (overflow)") + { + // n = INT64_MAX + 1 (0x8000000000000000) + // result = -1 - n = -9223372036854775809, which exceeds int64_t range + const std::vector input = {0x3B, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + json _; + CHECK_THROWS_WITH_AS(_ = json::from_cbor(input), + "[json.exception.parse_error.112] parse error at byte 9: syntax error while parsing CBOR value: negative integer overflow", + json::parse_error); + } + + SECTION("n = UINT64_MAX is rejected (overflow)") + { + // n = UINT64_MAX (0xFFFFFFFFFFFFFFFF) + // result = -1 - n = -18446744073709551616, which exceeds int64_t range + const std::vector input = {0x3B, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + json _; + CHECK_THROWS_WITH_AS(_ = json::from_cbor(input), + "[json.exception.parse_error.112] parse error at byte 9: syntax error while parsing CBOR value: negative integer overflow", + json::parse_error); + } + + SECTION("overflow with allow_exceptions=false returns discarded") + { + const std::vector input = {0x3B, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + const auto result = json::from_cbor(input, true, false); + CHECK(result.is_discarded()); + } + } + SECTION("tagged binary") { // create a binary value of subtype 42