Skip to content

Commit a9695f1

Browse files
PYTHON-5923 Add remaining-buffer bound check in _array_of_documents_to_buffer (#2872)
Signed-off-by: FugoP <264910004+AgentGymLeader@users.noreply.github.com>
1 parent 3eea205 commit a9695f1

3 files changed

Lines changed: 53 additions & 0 deletions

File tree

bson/_cbsonmodule.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3238,6 +3238,15 @@ static PyObject* _cbson_array_of_documents_to_buffer(PyObject* self, PyObject* a
32383238
goto fail;
32393239
}
32403240

3241+
if (value_length >= (uint32_t)(size - position)) {
3242+
PyObject* InvalidBSON = _error("InvalidBSON");
3243+
if (InvalidBSON) {
3244+
PyErr_SetString(InvalidBSON, "invalid array content");
3245+
Py_DECREF(InvalidBSON);
3246+
}
3247+
goto fail;
3248+
}
3249+
32413250
if (pymongo_buffer_write(buffer, string + position, value_length) == 1) {
32423251
goto fail;
32433252
}

doc/changelog.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ Changes in Version 4.18.0
1717
attempts, so consumers can correlate a retried operation's events. As a
1818
result, ``operation_id`` is no longer equal to the per-attempt ``request_id``
1919
for these operations.
20+
- Fixed a potential out-of-bounds read in the C extension when decoding an
21+
array of BSON documents. An embedded document whose declared length exceeds
22+
the bytes remaining in the array now raises
23+
:class:`~bson.errors.InvalidBSON` instead of reading past the end of the
24+
buffer.
2025

2126
Changes in Version 4.17.0 (2026/04/20)
2227
--------------------------------------

test/test_bson.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1758,6 +1758,45 @@ def test_array_of_documents_to_buffer(self):
17581758
with self.assertRaises(InvalidBSON):
17591759
_array_of_documents_to_buffer(buf)
17601760

1761+
def test_array_of_documents_to_buffer_rejects_oversized_element(self):
1762+
# Regression test for the bound check in
1763+
# _cbson_array_of_documents_to_buffer: an embedded document whose
1764+
# declared length exceeds the bytes remaining in the array document
1765+
# must raise InvalidBSON before any copy, rather than reading out of
1766+
# bounds (CWE-125).
1767+
doc = dict(a=1)
1768+
valid = encode({"0": doc})
1769+
# A well-formed buffer is unaffected by the guard.
1770+
self.assertEqual(_array_of_documents_to_buffer(valid), encode(doc))
1771+
# The first embedded document starts after the array header:
1772+
# 4-byte array length + 1-byte element type (0x03) + "0\x00" key = 7.
1773+
# Its leading 4 bytes are the embedded document's declared length.
1774+
offset = 4 + 1 + 2
1775+
malformed = bytearray(valid)
1776+
# Claim a length far larger than what remains (but >= BSON_MIN_SIZE so
1777+
# the existing lower-bound check still passes), forcing the new
1778+
# upper-bound guard to reject it.
1779+
malformed[offset : offset + 4] = struct.pack("<i", 0x7FFF)
1780+
# The C fast-path raises a dedicated "invalid array content" error from the
1781+
# new bound check; the pure-Python path reaches the same invariant through
1782+
# _get_object_size, which raises "invalid object length".
1783+
expected = "invalid array content" if bson.has_c() else "invalid object length"
1784+
with self.assertRaisesRegex(InvalidBSON, expected):
1785+
_array_of_documents_to_buffer(bytes(malformed))
1786+
1787+
def test_array_of_documents_to_buffer_rejects_element_consuming_terminator(self):
1788+
doc = dict(a=1)
1789+
valid = encode({"0": doc})
1790+
self.assertEqual(_array_of_documents_to_buffer(valid), encode(doc))
1791+
offset = 4 + 1 + 2
1792+
malformed = bytearray(valid)
1793+
value_length = len(valid) - offset
1794+
malformed[offset : offset + 4] = struct.pack("<i", value_length)
1795+
# Covers the exact boundary where the embedded doc consumes the array's EOO byte.
1796+
expected = "invalid array content" if bson.has_c() else "invalid object length"
1797+
with self.assertRaisesRegex(InvalidBSON, expected):
1798+
_array_of_documents_to_buffer(bytes(malformed))
1799+
17611800
def test_datetime_ms_hash(self):
17621801
# Equal values must have equal hashes.
17631802
self.assertEqual(hash(DatetimeMS(0)), hash(DatetimeMS(0)))

0 commit comments

Comments
 (0)