From 32f53adaaa7456cf3f5da86310dab55b87643a26 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 13 Oct 2026 11:07:53 -0500 Subject: [PATCH 1/2] PYTHON-6163 Pure-Python decode_all silently accepts 1-byte invalid BSON --- bson/__init__.py | 8 ++++++-- test/test_bson.py | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/bson/__init__.py b/bson/__init__.py index f2ca315b26..3f0d939a29 100644 --- a/bson/__init__.py +++ b/bson/__init__.py @@ -1133,11 +1133,15 @@ def _decode_all(data: _ReadableBuffer, opts: CodecOptions[_DocumentType]) -> lis data_len = len(data) docs: list[_DocumentType] = [] position = 0 - end = data_len - 1 use_raw = _raw_document_class(opts.document_class) try: - while position < end: + while position < data_len: + # A document is at minimum 5 bytes: the 4-byte size and the EOO. + if data_len - position < 5: + raise InvalidBSON("not enough data for a BSON document") obj_size = _UNPACK_INT_FROM(data, position)[0] + if obj_size < 5: + raise InvalidBSON("invalid message size") if data_len - position < obj_size: raise InvalidBSON( f"invalid object size: expected {obj_size}, got {data_len - position}" diff --git a/test/test_bson.py b/test/test_bson.py index 38e0d8c8d3..a19efb24e1 100644 --- a/test/test_bson.py +++ b/test/test_bson.py @@ -495,6 +495,32 @@ def test_element_overlaps_document_terminator(self): with self.assertRaises(InvalidBSON): decode_all(mm) + def test_decode_all_partial_document(self): + # PYTHON-6163: decode_all() must reject buffers ending in a partial + # document, matching the C extension, which raises InvalidBSON when + # fewer than 5 bytes (the minimum BSON document size) remain. + doc = encode({"a": 1}) + partial_bsons = [ + # A bare 1-byte buffer. + ("1-byte buffer", b"\x00"), + # 2-4 byte buffers, too short for even the size field. + ("2-byte buffer", b"\x05\x00"), + ("3-byte buffer", b"\x05\x00\x00"), + ("4-byte buffer", b"\x05\x00\x00\x00"), + # A valid document followed by a 1-byte tail. + ("1-byte tail", doc + b"\x00"), + ] + for description, data in partial_bsons: + with self.subTest(description, data=data): + with self.assertRaisesRegex(InvalidBSON, "not enough data for a BSON document"): + decode_all(data) + with self.assertRaisesRegex(InvalidBSON, "not enough data for a BSON document"): + decode_all(bytearray(data)) + with self.assertRaisesRegex(InvalidBSON, "not enough data for a BSON document"): + decode_all(memoryview(data)) + # Empty input is not an error. + self.assertEqual(decode_all(b""), []) + def test_data_timestamp(self): self.assertEqual( {"test": Timestamp(4, 20)}, From 5653b622d2aa0d214f1b311a49ae0cfff01d3c9f Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 13 Oct 2026 11:30:08 -0500 Subject: [PATCH 2/2] Match C decoder error message in pure-Python decode_all --- bson/__init__.py | 4 +--- doc/changelog.rst | 4 ++++ test/test_bson.py | 27 +++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/bson/__init__.py b/bson/__init__.py index 3f0d939a29..41865d2fd9 100644 --- a/bson/__init__.py +++ b/bson/__init__.py @@ -1143,9 +1143,7 @@ def _decode_all(data: _ReadableBuffer, opts: CodecOptions[_DocumentType]) -> lis if obj_size < 5: raise InvalidBSON("invalid message size") if data_len - position < obj_size: - raise InvalidBSON( - f"invalid object size: expected {obj_size}, got {data_len - position}" - ) + raise InvalidBSON("objsize too large") obj_end = position + obj_size - 1 if data[obj_end] != 0: raise InvalidBSON("bad eoo") diff --git a/doc/changelog.rst b/doc/changelog.rst index 212c9d946d..cea0a44699 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -69,10 +69,14 @@ Bug fixes - Fixed a bug where the C extension's BSON decoder accepted documents whose element data overlaps the document terminator instead of raising ``InvalidBSON`` (`PYTHON-6111`_). +- Fixed a bug where the pure-Python ``decode_all()`` accepted buffers ending + in a partial document instead of raising ``InvalidBSON``, now matching the + C extension (`PYTHON-6163`_). .. _PYTHON-6074: https://jira.mongodb.org/browse/PYTHON-6074 .. _PYTHON-6040: https://jira.mongodb.org/browse/PYTHON-6040 .. _PYTHON-6111: https://jira.mongodb.org/browse/PYTHON-6111 +.. _PYTHON-6163: https://jira.mongodb.org/browse/PYTHON-6163 Changes in Version 4.18.2 (2026/09/24) -------------------------------------- diff --git a/test/test_bson.py b/test/test_bson.py index a19efb24e1..392751ef7f 100644 --- a/test/test_bson.py +++ b/test/test_bson.py @@ -521,6 +521,33 @@ def test_decode_all_partial_document(self): # Empty input is not an error. self.assertEqual(decode_all(b""), []) + def test_decode_all_truncated_document(self): + # PYTHON-6163: decode_all() must reject documents declaring more + # bytes than remain, matching the C extension, which raises + # InvalidBSON("objsize too large"). + doc = encode({"a": 1}) + truncated_bsons = [ + # Declares 12 bytes, only 7 present. + ("truncated first document", b"\x0c\x00\x00\x00\x10a\x00"), + # A valid document followed by a truncated one. + ("truncated second document", doc + b"\x0c\x00\x00\x00\x10a\x00"), + ] + for description, data in truncated_bsons: + for buffer in (data, bytearray(data), memoryview(data)): + with self.subTest(description, buffer_type=type(buffer).__name__): + with self.assertRaisesRegex(InvalidBSON, "objsize too large"): + decode_all(buffer) + + def test_decode_all_invalid_message_size(self): + # PYTHON-6163: a 5-byte buffer whose declared size is below the + # minimum must raise InvalidBSON("invalid message size"), matching + # the C extension. + data = b"\x04\x00\x00\x00\x00" + for buffer in (data, bytearray(data), memoryview(data)): + with self.subTest(buffer_type=type(buffer).__name__): + with self.assertRaisesRegex(InvalidBSON, "invalid message size"): + decode_all(buffer) + def test_data_timestamp(self): self.assertEqual( {"test": Timestamp(4, 20)},