From e51c954fec4bd3c6f6c4768a20afd8d06b8c59ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sun, 1 Mar 2026 16:49:59 +0200 Subject: [PATCH] Added the max_depth decoder parameter CVE: CVE-2026-26209 Upstream-Status: Backport [https://github.com/agronholm/cbor2/commit/bcb6cea4edde1d00ff4f0eece883dea951f66e1b] Backport Changes: - Omitted docs/versionhistory.rst after it failed to cherry-pick because Scarthgap 5.6.4 lacks the later release sections; all source and test changes are retained. (cherry picked from commit bcb6cea4edde1d00ff4f0eece883dea951f66e1b) Signed-off-by: Devansh Patel --- source/decoder.c | 26 +++++++++++++++----------- source/decoder.h | 2 ++ tests/test_decoder.py | 15 +++++++++++++++ 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/source/decoder.c b/source/decoder.c index f8adc93..04c9142 100644 --- a/source/decoder.c +++ b/source/decoder.c @@ -152,6 +152,7 @@ CBORDecoder_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) Py_INCREF(Py_None); self->object_hook = Py_None; self->str_errors = PyBytes_FromString("strict"); + self->max_depth = CBOR2_DEFAULT_MAX_DEPTH; self->immutable = false; self->shared_index = -1; self->decode_depth = 0; @@ -169,19 +170,19 @@ error: // CBORDecoder.__init__(self, fp=None, tag_hook=None, object_hook=None, -// str_errors='strict', read_size=1) +// str_errors='strict', read_size=1, *, max_depth=100) int CBORDecoder_init(CBORDecoderObject *self, PyObject *args, PyObject *kwargs) { static char *keywords[] = { - "fp", "tag_hook", "object_hook", "str_errors", "read_size", NULL + "fp", "tag_hook", "object_hook", "str_errors", "read_size", "max_depth", NULL }; PyObject *fp = NULL, *tag_hook = NULL, *object_hook = NULL, *str_errors = NULL; Py_ssize_t read_size = CBOR2_DEFAULT_READ_SIZE; - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|OOOn", keywords, - &fp, &tag_hook, &object_hook, &str_errors, &read_size)) + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|OOOnn", keywords, + &fp, &tag_hook, &object_hook, &str_errors, &read_size, &self->max_depth)) return -1; if (read_size < 1) { @@ -2159,9 +2160,17 @@ decode(CBORDecoderObject *self, DecodeOptions options) self->shared_index = -1; } + if (self->decode_depth == self->max_depth) { + PyErr_Format( + _CBOR2_CBORDecodeError, + "maximum container nesting depth (%u) exceeded", self->max_depth); + return NULL; + } + if (Py_EnterRecursiveCall(" in CBORDecoder.decode")) return NULL; + self->decode_depth++; if (self->fp_read(self, &lead.byte, 1) == 0) { switch (lead.major) { case 0: ret = decode_uint(self, lead.subtype); break; @@ -2177,6 +2186,8 @@ decode(CBORDecoderObject *self, DecodeOptions options) } Py_LeaveRecursiveCall(); + self->decode_depth--; + if (options & DECODE_IMMUTABLE) self->immutable = old_immutable; if (options & DECODE_UNSHARED) @@ -2201,10 +2212,7 @@ PyObject * CBORDecoder_decode(CBORDecoderObject *self) { PyObject *ret; - self->decode_depth++; ret = decode(self, DECODE_NORMAL); - self->decode_depth--; - assert(self->decode_depth >= 0); if (self->decode_depth == 0) { clear_shareable_state(self); } @@ -2228,7 +2236,6 @@ CBORDecoder_decode_from_bytes(CBORDecoderObject *self, PyObject *data) if (!buf) return NULL; - self->decode_depth++; save_read = self->read; Py_INCREF(save_read); // Keep alive while we use a different read method save_read_pos = self->read_pos; @@ -2248,7 +2255,6 @@ CBORDecoder_decode_from_bytes(CBORDecoderObject *self, PyObject *data) } Py_DECREF(save_read); Py_DECREF(buf); - self->decode_depth--; return NULL; } @@ -2257,7 +2263,6 @@ CBORDecoder_decode_from_bytes(CBORDecoderObject *self, PyObject *data) Py_XDECREF(self->read); // Decrement BytesIO read method self->read = save_read; // Restore saved read (already has correct refcount) Py_DECREF(buf); - self->decode_depth--; if (is_nested) { PyMem_Free(self->readahead); @@ -2266,7 +2271,6 @@ CBORDecoder_decode_from_bytes(CBORDecoderObject *self, PyObject *data) self->read_pos = save_read_pos; self->read_len = save_read_len; - assert(self->decode_depth >= 0); if (self->decode_depth == 0) { clear_shareable_state(self); } diff --git a/source/decoder.h b/source/decoder.h index 3efff8b..6d465a4 100644 --- a/source/decoder.h +++ b/source/decoder.h @@ -6,6 +6,7 @@ // Default readahead buffer size for streaming reads. // Set to 1 for backwards compatibility (no buffering). #define CBOR2_DEFAULT_READ_SIZE 1 +#define CBOR2_DEFAULT_MAX_DEPTH 500 // Forward declaration for function pointer typedef struct CBORDecoderObject_; @@ -21,6 +22,7 @@ typedef struct CBORDecoderObject_ { PyObject *shareables; PyObject *stringref_namespace; PyObject *str_errors; + ssize_t max_depth; bool immutable; Py_ssize_t shared_index; Py_ssize_t decode_depth; diff --git a/tests/test_decoder.py b/tests/test_decoder.py index c5d1a9c..e0631af 100644 --- a/tests/test_decoder.py +++ b/tests/test_decoder.py @@ -138,6 +138,21 @@ def test_stream_position_after_decode(impl): assert stream.read() == extra_data +class TestMaximumDepth: + def test_default(self, impl) -> None: + with pytest.raises( + impl.CBORDecodeError, + match="maximum container nesting depth \\(500\\) exceeded", + ): + impl.loads(b"\x81" * 1000 + b"\x80") + + def test_explicit(self, impl) -> None: + with pytest.raises( + impl.CBORDecodeError, match=r"maximum container nesting depth \(9\) exceeded" + ): + impl.loads(b"\x81" * 10 + b"\x80", max_depth=9) + + @pytest.mark.parametrize( "payload, expected", [