Skip to content

Commit bda5371

Browse files
authored
Reject MessagePack/BSON binary subtypes that don't fit their wire format
Both formats store byte_container_with_subtype's subtype (a uint64_t) in a single byte. The writers cast to std::int8_t/std::uint8_t without a range check, so subtypes above 255 were silently truncated modulo 256 instead of raising an error. Throw out_of_range.413 instead when the subtype exceeds the representable range of 0-255. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
1 parent eaf52bd commit bda5371

4 files changed

Lines changed: 59 additions & 0 deletions

File tree

docs/mkdocs/docs/home/exceptions.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,17 @@ BSON stores the length of documents, arrays, strings, and binary values in a sig
933933
[`to_bson`](../api/basic_json/to_bson.md) produced documents with negative length prefixes that
934934
[`from_bson`](../api/basic_json/from_bson.md) rejected.
935935

936+
### json.exception.out_of_range.413
937+
938+
MessagePack's ext type and BSON's binary subtype are each stored in a single byte. This exception is thrown when serializing a
939+
[`byte_container_with_subtype`](../api/byte_container_with_subtype/index.md) whose subtype exceeds 255.
940+
941+
!!! failure "Example message"
942+
943+
```
944+
[json.exception.out_of_range.413] subtype 70000 is too large for the MessagePack ext type (max 255)
945+
```
946+
936947
## Further exceptions
937948

938949
This exception is thrown in case of errors that cannot be classified with the

include/nlohmann/detail/output/binary_writer.hpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,11 @@ class binary_writer
688688
// step 1.5: if this is an ext type, write the subtype
689689
if (use_ext)
690690
{
691+
if (JSON_HEDLEY_UNLIKELY(j.m_data.m_value.binary->subtype() > (std::numeric_limits<std::uint8_t>::max)()))
692+
{
693+
JSON_THROW(out_of_range::create(413, concat("subtype ", std::to_string(j.m_data.m_value.binary->subtype()), " is too large for the MessagePack ext type (max 255)"), &j));
694+
}
695+
691696
write_number(static_cast<std::int8_t>(j.m_data.m_value.binary->subtype()));
692697
}
693698

@@ -1187,6 +1192,12 @@ class binary_writer
11871192
write_bson_entry_header(name, 0x05);
11881193

11891194
write_number<std::int32_t>(to_bson_length(value.size()), true);
1195+
1196+
if (value.has_subtype() && JSON_HEDLEY_UNLIKELY(value.subtype() > (std::numeric_limits<std::uint8_t>::max)()))
1197+
{
1198+
JSON_THROW(out_of_range::create(413, concat("subtype ", std::to_string(value.subtype()), " is too large for the BSON binary subtype (max 255)"), nullptr));
1199+
}
1200+
11901201
write_number(value.has_subtype() ? static_cast<std::uint8_t>(value.subtype()) : static_cast<std::uint8_t>(0x00));
11911202

11921203
oa->write_characters(reinterpret_cast<const CharType*>(value.data()), value.size());

single_include/nlohmann/json.hpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17775,6 +17775,11 @@ class binary_writer
1777517775
// step 1.5: if this is an ext type, write the subtype
1777617776
if (use_ext)
1777717777
{
17778+
if (JSON_HEDLEY_UNLIKELY(j.m_data.m_value.binary->subtype() > (std::numeric_limits<std::uint8_t>::max)()))
17779+
{
17780+
JSON_THROW(out_of_range::create(413, concat("subtype ", std::to_string(j.m_data.m_value.binary->subtype()), " is too large for the MessagePack ext type (max 255)"), &j));
17781+
}
17782+
1777817783
write_number(static_cast<std::int8_t>(j.m_data.m_value.binary->subtype()));
1777917784
}
1778017785

@@ -18274,6 +18279,12 @@ class binary_writer
1827418279
write_bson_entry_header(name, 0x05);
1827518280

1827618281
write_number<std::int32_t>(to_bson_length(value.size()), true);
18282+
18283+
if (value.has_subtype() && JSON_HEDLEY_UNLIKELY(value.subtype() > (std::numeric_limits<std::uint8_t>::max)()))
18284+
{
18285+
JSON_THROW(out_of_range::create(413, concat("subtype ", std::to_string(value.subtype()), " is too large for the BSON binary subtype (max 255)"), nullptr));
18286+
}
18287+
1827718288
write_number(value.has_subtype() ? static_cast<std::uint8_t>(value.subtype()) : static_cast<std::uint8_t>(0x00));
1827818289

1827918290
oa->write_characters(reinterpret_cast<const CharType*>(value.data()), value.size());

tests/src/unit-regression2.cpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1668,4 +1668,30 @@ TEST_CASE("regression test - excessive binary container size honors allow_except
16681668
CHECK(json::from_cbor(std::vector<std::uint8_t> {0x9b, 0, 0, 0, 0, 0, 0, 0, 0x02}, true, false).is_discarded());
16691669
}
16701670

1671+
TEST_CASE("regression test - MessagePack/BSON writers reject binary subtypes that don't fit their wire format")
1672+
{
1673+
// MessagePack: subtype 0-255 must still round-trip correctly (regression guard, pre-existing behavior)
1674+
CHECK(json::from_msgpack(json::to_msgpack(json::binary({1, 2}, 0))).get_binary().subtype() == 0);
1675+
CHECK(json::from_msgpack(json::to_msgpack(json::binary({1, 2}, 200))).get_binary().subtype() == 200);
1676+
CHECK(json::from_msgpack(json::to_msgpack(json::binary({1, 2}, 255))).get_binary().subtype() == 255);
1677+
1678+
// MessagePack: subtype > 255 must now throw instead of silently truncating
1679+
CHECK_THROWS_AS(json::to_msgpack(json::binary({1, 2}, 256)), json::out_of_range);
1680+
CHECK_THROWS_AS(json::to_msgpack(json::binary({1, 2}, 70000)), json::out_of_range);
1681+
CHECK_THROWS_WITH_AS(json::to_msgpack(json::binary({1, 2}, 70000)), "[json.exception.out_of_range.413] subtype 70000 is too large for the MessagePack ext type (max 255)", json::out_of_range);
1682+
1683+
// BSON: same pattern
1684+
json doc255 = {{"b", json::binary({1, 2}, 255)}};
1685+
CHECK(json::from_bson(json::to_bson(doc255))["b"].get_binary().subtype() == 255);
1686+
CHECK_THROWS_AS(json::to_bson(json{{"b", json::binary({1, 2}, 256)}}), json::out_of_range);
1687+
CHECK_THROWS_AS(json::to_bson(json{{"b", json::binary({1, 2}, 300)}}), json::out_of_range);
1688+
CHECK_THROWS_WITH_AS(json::to_bson(json{{"b", json::binary({1, 2}, 300)}}), "[json.exception.out_of_range.413] subtype 300 is too large for the BSON binary subtype (max 255)", json::out_of_range);
1689+
1690+
// CBOR must remain unaffected (full 64-bit subtype range already supported correctly) - regression guard
1691+
CHECK(json::from_cbor(json::to_cbor(json::binary({1, 2}, 70000)), true, true, json::cbor_tag_handler_t::store).get_binary().subtype() == 70000);
1692+
1693+
// a binary value with NO subtype at all must be completely unaffected by this change
1694+
CHECK(json::from_msgpack(json::to_msgpack(json::binary({1, 2}))).get_binary().has_subtype() == false);
1695+
}
1696+
16711697
DOCTEST_CLANG_SUPPRESS_WARNING_POP

0 commit comments

Comments
 (0)