Skip to content

Commit bcb6cea

Browse files
committed
Added the max_depth decoder parameter
1 parent e61a5f3 commit bcb6cea

File tree

4 files changed

+34
-11
lines changed

4 files changed

+34
-11
lines changed

docs/versionhistory.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.
77

88
**UNRELEASED**
99

10+
- Added the ``max_depth`` decoder parameter to limit the maximum allowed nesting level of
11+
containers (CVE-2026-26209)
1012
- Changed the default ``read_size`` from 4096 to 1 for backwards compatibility.
1113
The buffered reads introduced in 5.8.0 could cause issues when code needs to
1214
access the stream position after decoding. Users can opt-in to faster decoding

source/decoder.c

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ CBORDecoder_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
153153
Py_INCREF(Py_None);
154154
self->object_hook = Py_None;
155155
self->str_errors = PyBytes_FromString("strict");
156+
self->max_depth = CBOR2_DEFAULT_MAX_DEPTH;
156157
self->immutable = false;
157158
self->shared_index = -1;
158159
self->decode_depth = 0;
@@ -170,19 +171,19 @@ CBORDecoder_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
170171

171172

172173
// CBORDecoder.__init__(self, fp=None, tag_hook=None, object_hook=None,
173-
// str_errors='strict', read_size=1)
174+
// str_errors='strict', read_size=1, *, max_depth=100)
174175
int
175176
CBORDecoder_init(CBORDecoderObject *self, PyObject *args, PyObject *kwargs)
176177
{
177178
static char *keywords[] = {
178-
"fp", "tag_hook", "object_hook", "str_errors", "read_size", NULL
179+
"fp", "tag_hook", "object_hook", "str_errors", "read_size", "max_depth", NULL
179180
};
180181
PyObject *fp = NULL, *tag_hook = NULL, *object_hook = NULL,
181182
*str_errors = NULL;
182183
Py_ssize_t read_size = CBOR2_DEFAULT_READ_SIZE;
183184

184-
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|OOOn", keywords,
185-
&fp, &tag_hook, &object_hook, &str_errors, &read_size))
185+
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|OOOnn", keywords,
186+
&fp, &tag_hook, &object_hook, &str_errors, &read_size, &self->max_depth))
186187
return -1;
187188

188189
if (read_size < 1) {
@@ -2184,9 +2185,17 @@ decode(CBORDecoderObject *self, DecodeOptions options)
21842185
self->shared_index = -1;
21852186
}
21862187

2188+
if (self->decode_depth == self->max_depth) {
2189+
PyErr_Format(
2190+
_CBOR2_CBORDecodeError,
2191+
"maximum container nesting depth (%u) exceeded", self->max_depth);
2192+
return NULL;
2193+
}
2194+
21872195
if (Py_EnterRecursiveCall(" in CBORDecoder.decode"))
21882196
return NULL;
21892197

2198+
self->decode_depth++;
21902199
if (self->fp_read(self, &lead.byte, 1) == 0) {
21912200
switch (lead.major) {
21922201
case 0: ret = decode_uint(self, lead.subtype); break;
@@ -2202,6 +2211,8 @@ decode(CBORDecoderObject *self, DecodeOptions options)
22022211
}
22032212

22042213
Py_LeaveRecursiveCall();
2214+
self->decode_depth--;
2215+
22052216
if (options & DECODE_IMMUTABLE)
22062217
self->immutable = old_immutable;
22072218
if (options & DECODE_UNSHARED)
@@ -2226,10 +2237,7 @@ PyObject *
22262237
CBORDecoder_decode(CBORDecoderObject *self)
22272238
{
22282239
PyObject *ret;
2229-
self->decode_depth++;
22302240
ret = decode(self, DECODE_NORMAL);
2231-
self->decode_depth--;
2232-
assert(self->decode_depth >= 0);
22332241
if (self->decode_depth == 0) {
22342242
clear_shareable_state(self);
22352243
}
@@ -2253,7 +2261,6 @@ CBORDecoder_decode_from_bytes(CBORDecoderObject *self, PyObject *data)
22532261
if (!buf)
22542262
return NULL;
22552263

2256-
self->decode_depth++;
22572264
save_read = self->read;
22582265
Py_INCREF(save_read); // Keep alive while we use a different read method
22592266
save_read_pos = self->read_pos;
@@ -2273,7 +2280,6 @@ CBORDecoder_decode_from_bytes(CBORDecoderObject *self, PyObject *data)
22732280
}
22742281
Py_DECREF(save_read);
22752282
Py_DECREF(buf);
2276-
self->decode_depth--;
22772283
return NULL;
22782284
}
22792285

@@ -2282,7 +2288,6 @@ CBORDecoder_decode_from_bytes(CBORDecoderObject *self, PyObject *data)
22822288
Py_XDECREF(self->read); // Decrement BytesIO read method
22832289
self->read = save_read; // Restore saved read (already has correct refcount)
22842290
Py_DECREF(buf);
2285-
self->decode_depth--;
22862291

22872292
if (is_nested) {
22882293
PyMem_Free(self->readahead);
@@ -2291,7 +2296,6 @@ CBORDecoder_decode_from_bytes(CBORDecoderObject *self, PyObject *data)
22912296
self->read_pos = save_read_pos;
22922297
self->read_len = save_read_len;
22932298

2294-
assert(self->decode_depth >= 0);
22952299
if (self->decode_depth == 0) {
22962300
clear_shareable_state(self);
22972301
}

source/decoder.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
// Default readahead buffer size for streaming reads.
77
// Set to 1 for backwards compatibility (no buffering).
88
#define CBOR2_DEFAULT_READ_SIZE 1
9+
#define CBOR2_DEFAULT_MAX_DEPTH 500
910

1011
// Forward declaration for function pointer typedef
1112
struct CBORDecoderObject_;
@@ -21,6 +22,7 @@ typedef struct CBORDecoderObject_ {
2122
PyObject *shareables;
2223
PyObject *stringref_namespace;
2324
PyObject *str_errors;
25+
ssize_t max_depth;
2426
bool immutable;
2527
Py_ssize_t shared_index;
2628
Py_ssize_t decode_depth;

tests/test_decoder.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,21 @@ def test_stream_position_after_decode(impl):
138138
assert stream.read() == extra_data
139139

140140

141+
class TestMaximumDepth:
142+
def test_default(self, impl) -> None:
143+
with pytest.raises(
144+
impl.CBORDecodeError,
145+
match="maximum container nesting depth \\(500\\) exceeded",
146+
):
147+
impl.loads(b"\x81" * 1000 + b"\x80")
148+
149+
def test_explicit(self, impl) -> None:
150+
with pytest.raises(
151+
impl.CBORDecodeError, match=r"maximum container nesting depth \(9\) exceeded"
152+
):
153+
impl.loads(b"\x81" * 10 + b"\x80", max_depth=9)
154+
155+
141156
@pytest.mark.parametrize(
142157
"payload, expected",
143158
[

0 commit comments

Comments
 (0)