Skip to content

Commit acbde65

Browse files
authored
Limit CBOR deepness (#7838)
1 parent d5442e7 commit acbde65

6 files changed

Lines changed: 100 additions & 30 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
66
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
77

8+
## [7.0.1]
9+
10+
[7.0.1]: https://github.com/microsoft/CCF/releases/tag/ccf-7.0.1
11+
12+
### Fixed
13+
14+
- Fixed a bug in the CBOR parser where a deeply nested payload could cause a node to crash. That could be exploited, for instance, to cause a DoS on publicly avaliable COSE-authenticated endpoints (#7838).
15+
816
## [7.0.0]
917

1018
[7.0.0]: https://github.com/microsoft/CCF/releases/tag/ccf-7.0.0

doc/build_apps/migration_6_x_to_7_0.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,5 @@ Version Live Compatibility
3838
When upgrading CCF services from one major version to the next, our usual recommendation is to upgrade from ``N.latest`` to ``N+1.0.0``. Interoperation between other versions is not guaranteed.
3939

4040
.. note:: For upgrades from 6.x to 7.0 specifically, a minimum version of 6.0.21 is required before upgrading. While upgrading from the latest 6.x release is recommended for the best experience, 6.0.21 is the minimum supported version that ensures proper snapshot compatibility and consistent chunk sizes in 7.0.
41+
42+
Update from the latest `6.x` straight to `7.0.1` intead of `7.0.0` is highly recommended due to (#7838). Compatibility is preserved.

python/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "ccf"
7-
version = "7.0.0"
7+
version = "7.0.1"
88
authors = [
99
{ name="CCF Team", email="CCF-Sec@microsoft.com" },
1010
]

src/crypto/cbor.cpp

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ namespace
8181
std::list<std::vector<cbor_raw>> arrays;
8282
std::list<std::vector<cbor_map_entry>> maps;
8383
};
84-
Value consume(cbor_nondet_t cbor);
84+
Value consume(cbor_nondet_t cbor, size_t depth, size_t max_depth);
8585

8686
void print_indent(std::ostringstream& os, size_t indent)
8787
{
@@ -129,7 +129,7 @@ namespace
129129
return std::make_shared<ValueImpl>(value);
130130
}
131131

132-
Value consume_array(cbor_nondet_t cbor)
132+
Value consume_array(cbor_nondet_t cbor, size_t depth, size_t max_depth)
133133
{
134134
cbor_nondet_array_iterator_t iter;
135135
if (!cbor_nondet_array_iterator_start(cbor, &iter))
@@ -147,12 +147,12 @@ namespace
147147
throw CBORDecodeError(
148148
Error::DECODE_FAILED, "Failed to get next array item");
149149
}
150-
array.items.push_back(consume(item));
150+
array.items.push_back(consume(item, depth + 1, max_depth));
151151
}
152152
return std::make_shared<ValueImpl>(std::move(array));
153153
}
154154

155-
Value consume_map(cbor_nondet_t cbor)
155+
Value consume_map(cbor_nondet_t cbor, size_t depth, size_t max_depth)
156156
{
157157
cbor_map_iterator iter;
158158
if (!cbor_nondet_map_iterator_start(cbor, &iter))
@@ -171,12 +171,14 @@ namespace
171171
throw CBORDecodeError(
172172
Error::DECODE_FAILED, "Failed to get next map entry");
173173
}
174-
map.items.emplace_back(consume(key_raw), consume(value_raw));
174+
map.items.emplace_back(
175+
consume(key_raw, depth + 1, max_depth),
176+
consume(value_raw, depth + 1, max_depth));
175177
}
176178
return std::make_shared<ValueImpl>(std::move(map));
177179
}
178180

179-
Value consume_tagged(cbor_nondet_t cbor)
181+
Value consume_tagged(cbor_nondet_t cbor, size_t depth, size_t max_depth)
180182
{
181183
uint64_t tag = 0;
182184
cbor_nondet_t payload;
@@ -188,7 +190,7 @@ namespace
188190

189191
Tagged tagged;
190192
tagged.tag = tag;
191-
tagged.item = consume(payload);
193+
tagged.item = consume(payload, depth + 1, max_depth);
192194
return std::make_shared<ValueImpl>(std::move(tagged));
193195
}
194196

@@ -206,8 +208,15 @@ namespace
206208
return std::make_shared<ValueImpl>(value);
207209
}
208210

209-
Value consume(cbor_nondet_t cbor)
211+
Value consume(cbor_nondet_t cbor, size_t depth, size_t max_depth)
210212
{
213+
if (depth > max_depth)
214+
{
215+
throw CBORDecodeError(
216+
Error::DECODE_FAILED,
217+
fmt::format("Maximum CBOR nesting depth ({}) exceeded", max_depth));
218+
}
219+
211220
const auto mt = cbor_nondet_major_type(cbor);
212221
switch (mt)
213222
{
@@ -219,11 +228,11 @@ namespace
219228
case CBOR_MAJOR_TYPE_TEXT_STRING:
220229
return consume_text_string(cbor);
221230
case CBOR_MAJOR_TYPE_ARRAY:
222-
return consume_array(cbor);
231+
return consume_array(cbor, depth, max_depth);
223232
case CBOR_MAJOR_TYPE_MAP:
224-
return consume_map(cbor);
233+
return consume_map(cbor, depth, max_depth);
225234
case CBOR_MAJOR_TYPE_TAGGED:
226-
return consume_tagged(cbor);
235+
return consume_tagged(cbor, depth, max_depth);
227236
case CBOR_MAJOR_TYPE_SIMPLE_VALUE:
228237
return consume_simple(cbor);
229238
default:
@@ -249,7 +258,8 @@ namespace
249258
}
250259
}
251260

252-
cbor_raw to_raw_cbor(const Value& value, CborRawArena& arena);
261+
cbor_raw to_raw_cbor(
262+
const Value& value, CborRawArena& arena, size_t depth, size_t max_depth);
253263

254264
cbor_raw to_raw_signed(const Signed& v)
255265
{
@@ -295,10 +305,11 @@ namespace
295305
return result;
296306
}
297307

298-
cbor_raw to_raw_tagged(const Tagged& v, CborRawArena& arena)
308+
cbor_raw to_raw_tagged(
309+
const Tagged& v, CborRawArena& arena, size_t depth, size_t max_depth)
299310
{
300311
cbor_raw result;
301-
arena.push(to_raw_cbor(v.item, arena));
312+
arena.push(to_raw_cbor(v.item, arena, depth + 1, max_depth));
302313
if (!cbor_nondet_mk_tagged(v.tag, arena.single(), &result))
303314
{
304315
throw CBOREncodeError(
@@ -308,14 +319,15 @@ namespace
308319
return result;
309320
}
310321

311-
cbor_raw to_raw_array(const Array& v, CborRawArena& arena)
322+
cbor_raw to_raw_array(
323+
const Array& v, CborRawArena& arena, size_t depth, size_t max_depth)
312324
{
313325
cbor_raw result;
314326
std::vector<cbor_raw> items;
315327
items.reserve(v.items.size());
316328
for (const auto& item : v.items)
317329
{
318-
items.push_back(to_raw_cbor(item, arena));
330+
items.push_back(to_raw_cbor(item, arena, depth + 1, max_depth));
319331
}
320332

321333
size_t arr_size = items.size();
@@ -337,16 +349,17 @@ namespace
337349
return result;
338350
}
339351

340-
cbor_raw to_raw_map(const Map& v, CborRawArena& arena)
352+
cbor_raw to_raw_map(
353+
const Map& v, CborRawArena& arena, size_t depth, size_t max_depth)
341354
{
342355
cbor_raw result;
343356

344357
std::vector<cbor_map_entry> entries;
345358
entries.reserve(v.items.size());
346359
for (const auto& [key, value] : v.items)
347360
{
348-
auto cbor_key = to_raw_cbor(key, arena);
349-
auto cbor_value = to_raw_cbor(value, arena);
361+
auto cbor_key = to_raw_cbor(key, arena, depth + 1, max_depth);
362+
auto cbor_value = to_raw_cbor(value, arena, depth + 1, max_depth);
350363
entries.push_back(cbor_nondet_mk_map_entry(cbor_key, cbor_value));
351364
}
352365

@@ -369,8 +382,16 @@ namespace
369382
return result;
370383
}
371384

372-
cbor_raw to_raw_cbor(const Value& value, CborRawArena& arena)
385+
cbor_raw to_raw_cbor(
386+
const Value& value, CborRawArena& arena, size_t depth, size_t max_depth)
373387
{
388+
if (depth > max_depth)
389+
{
390+
throw CBOREncodeError(
391+
Error::ENCODE_FAILED,
392+
fmt::format("Maximum CBOR nesting depth ({}) exceeded", max_depth));
393+
}
394+
374395
return std::visit(
375396
[&](const auto& v) {
376397
using T = std::decay_t<decltype(v)>;
@@ -392,15 +413,15 @@ namespace
392413
}
393414
if constexpr (std::is_same_v<T, Tagged>)
394415
{
395-
return to_raw_tagged(v, arena);
416+
return to_raw_tagged(v, arena, depth, max_depth);
396417
}
397418
if constexpr (std::is_same_v<T, Array>)
398419
{
399-
return to_raw_array(v, arena);
420+
return to_raw_array(v, arena, depth, max_depth);
400421
}
401422
if constexpr (std::is_same_v<T, Map>)
402423
{
403-
return to_raw_map(v, arena);
424+
return to_raw_map(v, arena, depth, max_depth);
404425
}
405426
},
406427
value->value);
@@ -536,7 +557,7 @@ namespace ccf::cbor
536557
return std::make_shared<ValueImpl>(Map{.items = std::move(data)});
537558
}
538559

539-
Value parse(std::span<const uint8_t> raw)
560+
Value parse(std::span<const uint8_t> raw, size_t max_depth)
540561
{
541562
cbor_nondet_t cbor;
542563
const bool check_map_key_bound = false;
@@ -561,13 +582,13 @@ namespace ccf::cbor
561582
fmt::format("Trailing {} byte(s) after CBOR item", cbor_parse_size));
562583
}
563584

564-
return consume(cbor);
585+
return consume(cbor, 0, max_depth);
565586
}
566587

567-
std::vector<uint8_t> serialize(const Value& value)
588+
std::vector<uint8_t> serialize(const Value& value, size_t max_depth)
568589
{
569590
CborRawArena arena{};
570-
auto raw = to_raw_cbor(value, arena);
591+
auto raw = to_raw_cbor(value, arena, 0, max_depth);
571592
const auto expected_size =
572593
cbor_nondet_size(raw, std::numeric_limits<size_t>::max());
573594

src/crypto/cbor.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ namespace ccf::cbor
113113
Value make_array(std::vector<Value>&& data);
114114
Value make_map(std::vector<MapItem>&& data);
115115

116-
Value parse(std::span<const uint8_t> raw);
117-
std::vector<uint8_t> serialize(const Value& value);
116+
Value parse(std::span<const uint8_t> raw, size_t max_depth = 16);
117+
std::vector<uint8_t> serialize(const Value& value, size_t max_depth = 16);
118118

119119
std::string to_string(const Value& value);
120120
bool simple_to_boolean(const Simple& value);

src/crypto/test/cbor.cpp

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1739,3 +1739,42 @@ TEST_CASE("CBOR: trailing bytes rejected")
17391739
auto array_trailing = ccf::ds::from_hex("82010203");
17401740
REQUIRE_THROWS_AS(parse(array_trailing), CBORDecodeError);
17411741
}
1742+
1743+
TEST_CASE("CBOR: parse max depth")
1744+
{
1745+
// depth 1: [42] -- should pass at max_depth=2
1746+
auto depth1 = ccf::ds::from_hex("81182a");
1747+
REQUIRE_NOTHROW(parse(depth1, 2));
1748+
1749+
// depth 2: [[42]] -- should pass at max_depth=2
1750+
auto depth2 = ccf::ds::from_hex("8181182a");
1751+
REQUIRE_NOTHROW(parse(depth2, 2));
1752+
1753+
// depth 3: [[[42]]] -- should fail at max_depth=2
1754+
auto depth3 = ccf::ds::from_hex("818181182a");
1755+
REQUIRE_THROWS_AS(parse(depth3, 2), CBORDecodeError);
1756+
1757+
// map depth 3: {1: {1: {1: 42}}} -- should fail at max_depth=2
1758+
auto map_depth3 = ccf::ds::from_hex("a101a101a101182a");
1759+
REQUIRE_THROWS_AS(parse(map_depth3, 2), CBORDecodeError);
1760+
1761+
// map depth 2: {1: {1: 42}} -- should pass at max_depth=2
1762+
auto map_depth2 = ccf::ds::from_hex("a101a101182a");
1763+
REQUIRE_NOTHROW(parse(map_depth2, 2));
1764+
}
1765+
1766+
TEST_CASE("CBOR: serialize max depth")
1767+
{
1768+
// depth 1: [42] -- should pass at max_depth=1,2
1769+
auto shallow = make_array({make_signed(42)});
1770+
REQUIRE_NOTHROW(serialize(shallow, 1));
1771+
REQUIRE_NOTHROW(serialize(shallow, 2));
1772+
1773+
// Build 3 levels: [[[42]]]
1774+
auto deep = make_array({make_array({make_array({make_signed(42)})})});
1775+
REQUIRE_THROWS_AS(serialize(deep, 2), CBOREncodeError);
1776+
1777+
// Same deep tree passes at higher limit
1778+
REQUIRE_NOTHROW(serialize(deep, 3));
1779+
REQUIRE_NOTHROW(serialize(deep, 4));
1780+
}

0 commit comments

Comments
 (0)