Reject integer tokens in float nodes of fully checked images (#6084)
image_check::full accepted a float node whose token is an integer literal that fits 64 bits (e.g. 10446744073709551616 changed from 18446744073709551616). The parser stores an integer node for such a token, so the loaded document differed from what parsing the same text gives. check_float_ranges now requires an integer token of a float node not to fit uint64_t (or int64_t when negative). Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
1 parent
53d097f980
commit
9d35fe6d21
4 files changed
+96
-7
No files matched your search
@@ -113,11 +113,12 @@ How thoroughly `load()` validates `image` before trusting it.
|
|||||||
`full` (the default)
|
`full` (the default)
|
||||||
: Checks everything the parser itself guarantees: structure and bounds; that every string is valid UTF-8 (and, for a
|
: Checks everything the parser itself guarantees: structure and bounds; that every string is valid UTF-8 (and, for a
|
||||||
string still in the source text, that it contains no quote, backslash, or control character); and that every number
|
string still in the source text, that it contains no quote, backslash, or control character); and that every number
|
||||||
token is well-formed and matches the value stored for it. Strings and float tokens may share a range only if the
|
token is well-formed and matches the value stored for it, and has the type the parser gives it (a float node only
|
||||||
ranges are identical (as nodes that share a value do), and never overlap otherwise. Reading and serializing a
|
for a token with a fraction or an exponent, or an integer that does not fit 64 bits). Strings and float tokens may
|
||||||
checked image is safe and always produces valid JSON, exactly as for a parsed document. This is the right choice for
|
share a range only if the ranges are identical (as nodes that share a value do), and never overlap otherwise.
|
||||||
an image from anything you do not fully control — a file, a cache shared with other processes, a peer on the
|
Reading and serializing a checked image is safe and always produces valid JSON, exactly as for a parsed document.
|
||||||
network.
|
This is the right choice for an image from anything you do not fully control — a file, a cache shared with other
|
||||||
|
processes, a peer on the network.
|
||||||
|
|
||||||
`bounds`
|
`bounds`
|
||||||
: Checks structure and bounds only: that every offset and count in the node index stays inside the image. So
|
: Checks structure and bounds only: that every offset and count in the node index stays inside the image. So
|
||||||
|
|||||||
@@ -416,6 +416,28 @@ inline bool float_token_finite(const unsigned char* s, std::size_t len, const nu
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// whether an integer token (well-formed, no fraction or exponent) fits 64
|
||||||
|
/// bits: the parser stores a number_integer or number_unsigned node for it,
|
||||||
|
/// and a number_float node only for a token that does not fit
|
||||||
|
inline bool integer_token_fits(const number_token& t)
|
||||||
|
{
|
||||||
|
if (t.int_digits > 20)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (t.int_digits == 20)
|
||||||
|
{
|
||||||
|
// (20 digits are compared with 2^64 - 1; a negative one is below -2^63)
|
||||||
|
// (only a leading 1 needs the comparison: the first digit of 20 is not 0)
|
||||||
|
return !t.negative && *t.int_start == '1' && std::memcmp(t.int_start, "18446744073709551615", 20) <= 0;
|
||||||
|
}
|
||||||
|
if (t.negative && t.int_digits == 19)
|
||||||
|
{
|
||||||
|
return *t.int_start != '9' || std::memcmp(t.int_start, "9223372036854775808", 19) <= 0;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/// whether an integer node matches its token the way the parser records it
|
/// whether an integer node matches its token the way the parser records it
|
||||||
/// (after the bounds check; the token has at most 256 characters)
|
/// (after the bounds check; the token has at most 256 characters)
|
||||||
inline bool check_integer(const node& n, const unsigned char* text)
|
inline bool check_integer(const node& n, const unsigned char* text)
|
||||||
@@ -535,7 +557,8 @@ inline bool check_float_ranges(std::vector<float_range>& ranges, const unsigned
|
|||||||
if (!plain_float_token(s, r.len, r.extra))
|
if (!plain_float_token(s, r.len, r.extra))
|
||||||
{
|
{
|
||||||
number_token t{};
|
number_token t{};
|
||||||
if (!scan_number_token(s, r.len, t) || !float_token_finite(s, r.len, t))
|
// (an integer token is a float node only if it does not fit 64 bits)
|
||||||
|
if (!scan_number_token(s, r.len, t) || (!t.is_float && integer_token_fits(t)) || !float_token_finite(s, r.len, t))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+24
-1
@@ -7439,6 +7439,28 @@ inline bool float_token_finite(const unsigned char* s, std::size_t len, const nu
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// whether an integer token (well-formed, no fraction or exponent) fits 64
|
||||||
|
/// bits: the parser stores a number_integer or number_unsigned node for it,
|
||||||
|
/// and a number_float node only for a token that does not fit
|
||||||
|
inline bool integer_token_fits(const number_token& t)
|
||||||
|
{
|
||||||
|
if (t.int_digits > 20)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (t.int_digits == 20)
|
||||||
|
{
|
||||||
|
// (20 digits are compared with 2^64 - 1; a negative one is below -2^63)
|
||||||
|
// (only a leading 1 needs the comparison: the first digit of 20 is not 0)
|
||||||
|
return !t.negative && *t.int_start == '1' && std::memcmp(t.int_start, "18446744073709551615", 20) <= 0;
|
||||||
|
}
|
||||||
|
if (t.negative && t.int_digits == 19)
|
||||||
|
{
|
||||||
|
return *t.int_start != '9' || std::memcmp(t.int_start, "9223372036854775808", 19) <= 0;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/// whether an integer node matches its token the way the parser records it
|
/// whether an integer node matches its token the way the parser records it
|
||||||
/// (after the bounds check; the token has at most 256 characters)
|
/// (after the bounds check; the token has at most 256 characters)
|
||||||
inline bool check_integer(const node& n, const unsigned char* text)
|
inline bool check_integer(const node& n, const unsigned char* text)
|
||||||
@@ -7558,7 +7580,8 @@ inline bool check_float_ranges(std::vector<float_range>& ranges, const unsigned
|
|||||||
if (!plain_float_token(s, r.len, r.extra))
|
if (!plain_float_token(s, r.len, r.extra))
|
||||||
{
|
{
|
||||||
number_token t{};
|
number_token t{};
|
||||||
if (!scan_number_token(s, r.len, t) || !float_token_finite(s, r.len, t))
|
// (an integer token is a float node only if it does not fit 64 bits)
|
||||||
|
if (!scan_number_token(s, r.len, t) || (!t.is_float && integer_token_fits(t)) || !float_token_finite(s, r.len, t))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -759,6 +759,48 @@ TEST_CASE("json_view images: errors")
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SECTION("float tokens: an integer token is a float node only if it does not fit 64 bits")
|
||||||
|
{
|
||||||
|
for (const char* const token :
|
||||||
|
{
|
||||||
|
"18446744073709551616", "-9223372036854775809", "100000000000000000000", "-18446744073709551616", "123456789012345678901234567890"
|
||||||
|
})
|
||||||
|
{
|
||||||
|
CAPTURE(token)
|
||||||
|
const std::string text = std::string("[") + token + "]";
|
||||||
|
const json_document d = json_document::parse(text);
|
||||||
|
CHECK(d.root()[0].is_number_float());
|
||||||
|
const std::vector<std::uint8_t> image = d.save();
|
||||||
|
const std::size_t at = text_at(image) + 1; // the first byte of the token
|
||||||
|
CHECK(loaded_dump(image) == json::parse(text).dump());
|
||||||
|
for (std::size_t pos = token[0] == '-' ? 1 : 0; token[pos] != '\0'; ++pos) // (not the sign: that changes the digits)
|
||||||
|
{
|
||||||
|
for (char digit = '0'; digit <= '9'; ++digit)
|
||||||
|
{
|
||||||
|
std::vector<std::uint8_t> changed = image;
|
||||||
|
changed[at + pos] = static_cast<std::uint8_t>(digit);
|
||||||
|
std::string t = token;
|
||||||
|
t[pos] = digit;
|
||||||
|
CAPTURE(pos)
|
||||||
|
CAPTURE(digit)
|
||||||
|
CAPTURE(t)
|
||||||
|
// the parser stores an integer for the changed token if it is one (and fits)
|
||||||
|
const bool float_node = json::accept(t) && json::parse(t).is_number_float();
|
||||||
|
const std::string result = load_result(changed, image_check::full);
|
||||||
|
if (float_node)
|
||||||
|
{
|
||||||
|
CHECK(result.empty());
|
||||||
|
CHECK(loaded_dump(changed) == json::parse("[" + t + "]").dump());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CHECK(result == check_failed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SECTION("invalid UTF-8 of a decoded string: the compact dump writes it, the others throw")
|
SECTION("invalid UTF-8 of a decoded string: the compact dump writes it, the others throw")
|
||||||
{
|
{
|
||||||
const json_document d = json_document::parse(R"({"k\n":"\u00e9x"})");
|
const json_document d = json_document::parse(R"({"k\n":"\u00e9x"})");
|
||||||
|
|||||||
Reference in new issue
Block a user