From 32349b379b5e7d228745655195625787a4857ec5 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 5 Sep 2026 21:57:41 +0200 Subject: [PATCH] Make contains(json_pointer) return false instead of throwing on unrepresentable array-index tokens contains(const json_pointer&) is documented to never throw, but a purely numeric reference token that is syntactically a valid array index yet numerically too large to be represented (exceeding size_type's max, or exceeding ULLONG_MAX and causing strtoull() to set errno to ERANGE) made it fall through to array_index(), which throws out_of_range.410/404. Pre-check the token's magnitude the same way array_index() does, but return false instead of throwing, mirroring how the surrounding code already rejects other malformed tokens (leading zero, non-digit characters, "-") without throwing. operator[]/at() are untouched and keep throwing for these inputs. Fixes #5395 Signed-off-by: Niels Lohmann --- include/nlohmann/detail/json_pointer.hpp | 14 ++++++++ single_include/nlohmann/json.hpp | 14 ++++++++ tests/src/unit-json_pointer.cpp | 42 ++++++++++++++++++++++++ 3 files changed, 70 insertions(+) diff --git a/include/nlohmann/detail/json_pointer.hpp b/include/nlohmann/detail/json_pointer.hpp index 576ba62cc..247c5babb 100644 --- a/include/nlohmann/detail/json_pointer.hpp +++ b/include/nlohmann/detail/json_pointer.hpp @@ -748,6 +748,20 @@ class json_pointer } } + // the reference token consists only of digits at this point (cf. checks + // above); however, its numeric value might not be representable, in which + // case array_index() would throw out_of_range.404/410 -- contains() must + // not throw (see #5395), so such a reference token is treated as "not found" + errno = 0; // strtoull() does not reset errno on success + char* p_end = nullptr; // NOLINT(misc-const-correctness) + const unsigned long long magnitude = std::strtoull(reference_token.c_str(), &p_end, 10); // NOLINT(runtime/int) + if (JSON_HEDLEY_UNLIKELY(errno == ERANGE // the value exceeds ULLONG_MAX + || magnitude >= static_cast((std::numeric_limits::max)()))) // NOLINT(runtime/int) + { + // the array index cannot be represented as size_type + return false; + } + const auto idx = array_index(reference_token); if (idx >= ptr->size()) { diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 31bfb869d..a8fff3e41 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -16394,6 +16394,20 @@ class json_pointer } } + // the reference token consists only of digits at this point (cf. checks + // above); however, its numeric value might not be representable, in which + // case array_index() would throw out_of_range.404/410 -- contains() must + // not throw (see #5395), so such a reference token is treated as "not found" + errno = 0; // strtoull() does not reset errno on success + char* p_end = nullptr; // NOLINT(misc-const-correctness) + const unsigned long long magnitude = std::strtoull(reference_token.c_str(), &p_end, 10); // NOLINT(runtime/int) + if (JSON_HEDLEY_UNLIKELY(errno == ERANGE // the value exceeds ULLONG_MAX + || magnitude >= static_cast((std::numeric_limits::max)()))) // NOLINT(runtime/int) + { + // the array index cannot be represented as size_type + return false; + } + const auto idx = array_index(reference_token); if (idx >= ptr->size()) { diff --git a/tests/src/unit-json_pointer.cpp b/tests/src/unit-json_pointer.cpp index a8ed4a89e..4082de45c 100644 --- a/tests/src/unit-json_pointer.cpp +++ b/tests/src/unit-json_pointer.cpp @@ -319,6 +319,44 @@ TEST_CASE("JSON pointers") CHECK_THROWS_WITH_AS(j[jp] = 1, throw_msg.c_str(), json::out_of_range&); CHECK_THROWS_WITH_AS(j_const[jp] == 1, throw_msg.c_str(), json::out_of_range&); + + // #5395: contains() must not throw for a reference token that is a + // syntactically valid array index but numerically exceeds ULLONG_MAX + // (causing strtoull() to set errno to ERANGE) -- it should just report + // that the pointer does not resolve to an element + CHECK(!j.contains(jp)); + CHECK(!j_const.contains(jp)); + } + + { + // #5395: same as above, but using the exact reproduction from the issue + json::json_pointer const jp("/99999999999999999999"); + std::string const throw_msg = "[json.exception.out_of_range.404] unresolved reference token '99999999999999999999'"; + + CHECK_THROWS_WITH_AS(j[jp] = 1, throw_msg.c_str(), json::out_of_range&); + CHECK_THROWS_WITH_AS(j_const[jp] == 1, throw_msg.c_str(), json::out_of_range&); + CHECK_THROWS_WITH_AS(j.at(jp) = 1, throw_msg.c_str(), json::out_of_range&); + CHECK_THROWS_WITH_AS(j_const.at(jp) == 1, throw_msg.c_str(), json::out_of_range&); + + CHECK(!j.contains(jp)); + CHECK(!j_const.contains(jp)); + } + + { + // #5395: a reference token that is numerically representable in + // unsigned long long but exceeds size_type's max (e.g. ULLONG_MAX + // itself on typical 64-bit platforms, where size_type's max equals + // ULLONG_MAX) must not make contains() throw either + json::json_pointer const jp("/18446744073709551615"); + std::string const throw_msg = "[json.exception.out_of_range.410] array index 18446744073709551615 exceeds size_type"; + + CHECK_THROWS_WITH_AS(j[jp] = 1, throw_msg.c_str(), json::out_of_range&); + CHECK_THROWS_WITH_AS(j_const[jp] == 1, throw_msg.c_str(), json::out_of_range&); + CHECK_THROWS_WITH_AS(j.at(jp) = 1, throw_msg.c_str(), json::out_of_range&); + CHECK_THROWS_WITH_AS(j_const.at(jp) == 1, throw_msg.c_str(), json::out_of_range&); + + CHECK(!j.contains(jp)); + CHECK(!j_const.contains(jp)); } // on some machines, the check below is not constant @@ -334,6 +372,10 @@ TEST_CASE("JSON pointers") CHECK_THROWS_WITH_AS(j[jp] = 1, throw_msg.c_str(), json::out_of_range&); CHECK_THROWS_WITH_AS(j_const[jp] == 1, throw_msg.c_str(), json::out_of_range&); + + // #5395: contains() must not throw for a reference token exceeding size_type's max + CHECK(!j.contains(jp)); + CHECK(!j_const.contains(jp)); } DOCTEST_MSVC_SUPPRESS_WARNING_POP