Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions bson/_cbsonmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -3238,6 +3238,15 @@ static PyObject* _cbson_array_of_documents_to_buffer(PyObject* self, PyObject* a
goto fail;
}

if (value_length > (uint32_t)(size - position)) {
PyObject* InvalidBSON = _error("InvalidBSON");
if (InvalidBSON) {
PyErr_SetString(InvalidBSON, "invalid array content");
Py_DECREF(InvalidBSON);
}
goto fail;
}

if (pymongo_buffer_write(buffer, string + position, value_length) == 1) {
goto fail;
}
Expand Down
5 changes: 5 additions & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ Changes in Version 4.18.0
to the same server, avoiding a full handshake on each new connection.
Session resumption is supported on all Python versions for synchronous clients
and on Python 3.11+ for async clients.
- Fixed a potential out-of-bounds read in the C extension when decoding an
array of BSON documents. An embedded document whose declared length exceeds
the bytes remaining in the array now raises
:class:`~bson.errors.InvalidBSON` instead of reading past the end of the
buffer.

Changes in Version 4.17.0 (2026/04/20)
--------------------------------------
Expand Down
26 changes: 26 additions & 0 deletions test/test_bson.py
Original file line number Diff line number Diff line change
Expand Up @@ -1758,6 +1758,32 @@ def test_array_of_documents_to_buffer(self):
with self.assertRaises(InvalidBSON):
_array_of_documents_to_buffer(buf)

def test_array_of_documents_to_buffer_rejects_oversized_element(self):
# Regression test for the bound check in
# _cbson_array_of_documents_to_buffer: an embedded document whose
# declared length exceeds the bytes remaining in the array document
# must raise InvalidBSON before any copy, rather than reading out of
# bounds (CWE-125).
doc = dict(a=1)
valid = encode({"0": doc})
# A well-formed buffer is unaffected by the guard.
self.assertEqual(_array_of_documents_to_buffer(valid), encode(doc))
# The first embedded document starts after the array header:
# 4-byte array length + 1-byte element type (0x03) + "0\x00" key = 7.
# Its leading 4 bytes are the embedded document's declared length.
offset = 4 + 1 + 2
malformed = bytearray(valid)
# Claim a length far larger than what remains (but >= BSON_MIN_SIZE so
# the existing lower-bound check still passes), forcing the new
# upper-bound guard to reject it.
malformed[offset : offset + 4] = struct.pack("<i", 0x7FFF)
# The C fast-path raises a dedicated "invalid array content" error from the
# new bound check; the pure-Python path reaches the same invariant through
# _get_object_size, which raises "invalid object length".
expected = "invalid array content" if bson.has_c() else "invalid object length"
with self.assertRaisesRegex(InvalidBSON, expected):
_array_of_documents_to_buffer(bytes(malformed))

def test_datetime_ms_hash(self):
# Equal values must have equal hashes.
self.assertEqual(hash(DatetimeMS(0)), hash(DatetimeMS(0)))
Expand Down
Loading