mirror of
https://github.com/nlohmann/json.git
synced 2026-10-27 08:18:55 +00:00
Fix BSON binary subtype 0x02 and check the declared document size earlier (#5930)
* Write and read the inner length of BSON binary subtype 0x02 The BSON specification defines the payload of the "binary (old)" subtype 0x02 as an int32 followed by that many bytes. to_bson wrote the plain payload instead, so decoders such as pymongo and libbson-based readers rejected the bytes, and from_bson returned the inner length as part of the payload (documented as lenient behavior in gh-5333). The writer now emits the outer length (payload + 4), the subtype, the inner int32 holding the payload size, and the payload. The reader requires an outer length of at least 4 and an inner length equal to the outer length minus 4 (the cases the bson-corpus lists as decode errors), reports parse error 112 otherwise, and returns only the payload. Both directions change together so that the round trip keeps working. The single-reservation fast path for scalar entries declines subtype 0x02 because it has no room for the inner length. This is a deliberate exception to the opt-in rule for behavior changes: the previous output was invalid BSON. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Check the declared BSON document size before reading its content The BSON reader compared a document's declared size with the number of bytes read only when it reached the terminator. The declared size never bounded the reads, so a SAX handler saw the keys and values of a document whose size was too small before parse error 112 was reported. The container frame now stores the position where the document ends according to its declared size. open_bson_document rejects a size below 5 (size prefix plus terminator) and an embedded document that does not end before the terminator of its enclosing document; the element loop rejects an element whose type byte leaves no room for the name terminator and the document terminator. The terminator check stays and still catches a declared size that is too large. No input that was accepted before is rejected now; documents with a wrong size are reported earlier, with fewer SAX events, a different byte position and, in three cases, a different message. The error id (parse_error.112) is unchanged and strict=false still does not skip the check. The tests that asserted the old messages and positions are updated, with the outer sizes of two inputs corrected so that they still exercise the embedded document. New tests cover the SAX events, a size below 5, and an embedded document that fits exactly. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Document BSON subtype 0x02, document size checks, and lenient reads Describe the inner length of binary subtype 0x02 that to_bson now writes and from_bson checks and removes. Add the document size checks (parse_error.112, not affected by strict=false) to bson.md and from_bson.md, and extend the list of lenient reads with the UUID and MD5 payload lengths (not checked), duplicate keys (last value wins), embedded NUL bytes in strings, and the ignored trailing bytes with strict=false. State that the members of an object are written in iteration order. Correct the statement that element names are never validated on read: they are checked and sanitized like string values by the error handler. Add the BSON message to the examples of out_of_range.415 and type_error.321, the subtype limit of 255 to the binary values page, CBOR to the formats that use byte_container_with_subtype, and fix a typo in the incomplete mapping box. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Regenerate single_include and apply astyle Result of make amalgamate (including make pretty) for the preceding changes. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Address review of the BSON documentation Describe exactly when a too small document size is detected and note that subtype 2 data written by earlier versions is not readable. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * docs: use the Oxford comma, drop discarded-value remarks Add the serial comma in a list added by this PR and remove the BSON example of type_error.321 (with the type_error.317 remark) so the docs do not mention discarded values for BSON; restore the original singular heading of that section. No tables were changed by this PR. Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix -Weffc++ in the BSON recorder test and a clang-tidy use-auto hit Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix clang-tidy readability-redundant-member-init in BSON test Signed-off-by: Niels Lohmann <mail@nlohmann.me> --------- Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
1 parent
0c3570d44a
commit
dc768a7d5d
10 files changed
+491
-35
No files matched your search
@@ -235,6 +235,9 @@ class binary_reader
|
||||
/// UBJSON/BJData: the type marker of an optimized container, so that
|
||||
/// its elements are read without one of their own; 0 otherwise
|
||||
char_int_type type_marker;
|
||||
/// BSON: the position right after this document, according to the
|
||||
/// size it declares
|
||||
std::size_t end_position = 0;
|
||||
/// BSON: the size this document declares, in bytes
|
||||
std::int32_t declared_size = 0;
|
||||
/// whether to close this container with end_object() or end_array()
|
||||
@@ -371,6 +374,23 @@ class binary_reader
|
||||
return false;
|
||||
}
|
||||
|
||||
// the smallest document is the size prefix and the terminator
|
||||
if (JSON_HEDLEY_UNLIKELY(document_size < 5))
|
||||
{
|
||||
return bson_document_size_error("document size ", document_size, " is less than 5");
|
||||
}
|
||||
|
||||
// the declared size bounds where the document ends: an embedded
|
||||
// document must end before the terminator of the document that contains it
|
||||
const auto unsigned_size = static_cast<std::size_t>(document_size);
|
||||
const std::size_t document_end = unsigned_size > (std::numeric_limits<std::size_t>::max)() - document_start
|
||||
? (std::numeric_limits<std::size_t>::max)()
|
||||
: document_start + unsigned_size;
|
||||
if (JSON_HEDLEY_UNLIKELY(!container_stack.empty() && document_end >= container_stack.back().end_position))
|
||||
{
|
||||
return bson_document_size_error("document size ", document_size, " exceeds the size of the enclosing document");
|
||||
}
|
||||
|
||||
if (JSON_HEDLEY_UNLIKELY(!enter_container(is_object, detail::unknown_size())))
|
||||
{
|
||||
return false;
|
||||
@@ -378,6 +398,7 @@ class binary_reader
|
||||
|
||||
container_frame& frame = container_stack.back();
|
||||
frame.start_position = document_start;
|
||||
frame.end_position = document_end;
|
||||
frame.declared_size = document_size;
|
||||
return true;
|
||||
}
|
||||
@@ -436,6 +457,13 @@ class binary_reader
|
||||
return false;
|
||||
}
|
||||
|
||||
// an element needs at least its name's terminator, and the
|
||||
// document its own terminator, within the declared size
|
||||
if (JSON_HEDLEY_UNLIKELY(chars_read + 2 > container_stack.back().end_position))
|
||||
{
|
||||
return bson_document_size_error("document size ", container_stack.back().declared_size, " is too small for its content");
|
||||
}
|
||||
|
||||
const std::size_t element_type_parse_position = chars_read;
|
||||
key.clear();
|
||||
if (JSON_HEDLEY_UNLIKELY(!get_bson_cstr(key)))
|
||||
@@ -584,9 +612,46 @@ class binary_reader
|
||||
}
|
||||
result.set_subtype(subtype);
|
||||
|
||||
// the "old binary" subtype 0x02 starts with an int32 that repeats the
|
||||
// size of the payload following it; the payload does not include it
|
||||
if (subtype == 0x02)
|
||||
{
|
||||
return get_bson_old_binary(len, result);
|
||||
}
|
||||
|
||||
return get_binary(len, result);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief Parses the payload of a BSON binary value with subtype 0x02.
|
||||
@param[in] len The length of the binary value, including the inner int32.
|
||||
@param[in,out] result A reference to the binary variable where the payload
|
||||
(without the inner int32) is to be stored.
|
||||
@pre len >= 0
|
||||
@return `true` if the inner length was present and matches @a len
|
||||
*/
|
||||
template<typename NumberType>
|
||||
bool get_bson_old_binary(const NumberType len, binary_t& result)
|
||||
{
|
||||
if (JSON_HEDLEY_UNLIKELY(len < 4))
|
||||
{
|
||||
return bson_length_error("length of binary subtype 0x02 must be at least 4, is ", len, "binary");
|
||||
}
|
||||
|
||||
std::int32_t inner_length{};
|
||||
if (JSON_HEDLEY_UNLIKELY((!get_number<std::int32_t, true>(inner_length))))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (JSON_HEDLEY_UNLIKELY(static_cast<std::int64_t>(inner_length) != static_cast<std::int64_t>(len) - 4))
|
||||
{
|
||||
return bson_length_error("inner length of binary subtype 0x02 does not match the payload size; inner length is ", inner_length, "binary");
|
||||
}
|
||||
|
||||
return get_binary(inner_length, result);
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief Read a BSON document element of the given @a element_type.
|
||||
@param[in] element_type The BSON element type, c.f. http://bsonspec.org/spec.html
|
||||
@@ -4399,6 +4464,12 @@ class binary_reader
|
||||
return last_byte_error(exception_id::unexpected_byte, concat("document size ", std::to_string(document_size), " does not match the number of bytes read (", std::to_string(bytes_read), ")"), "document");
|
||||
}
|
||||
|
||||
JSON_HEDLEY_NEVER_INLINE
|
||||
bool bson_document_size_error(const char* prefix, const std::int32_t document_size, const char* suffix) const
|
||||
{
|
||||
return last_byte_error(exception_id::unexpected_byte, concat(prefix, std::to_string(document_size), suffix), "document");
|
||||
}
|
||||
|
||||
JSON_HEDLEY_NEVER_INLINE
|
||||
bool bson_unsupported_type_error(const std::size_t position, const char_int_type element_type) const
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user