Skip to content
Closed
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
2 changes: 1 addition & 1 deletion bson/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1009,7 +1009,7 @@ def _dict_to_bson(
try:
elements.append(_element_to_bson(key, value, check_keys, opts))
except InvalidDocument as err:
raise InvalidDocument(f"Invalid document {doc} | {err}") from err
raise InvalidDocument(f"Invalid document: {err}", doc) from err
except AttributeError:
raise TypeError(f"encoder expected a mapping type but got: {doc!r}") from None

Expand Down
30 changes: 14 additions & 16 deletions bson/_cbsonmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1645,42 +1645,40 @@ static int write_raw_doc(buffer_t buffer, PyObject* raw, PyObject* _raw_str) {
}


/* Update Invalid Document error message to include doc.
/* Update Invalid Document error to include doc as a property.
*/
void handle_invalid_doc_error(PyObject* dict) {
PyObject *etype = NULL, *evalue = NULL, *etrace = NULL;
PyObject *msg = NULL, *dict_str = NULL, *new_msg = NULL;
PyObject *msg = NULL, *new_msg = NULL, *new_evalue = NULL;
PyErr_Fetch(&etype, &evalue, &etrace);
PyObject *InvalidDocument = _error("InvalidDocument");
if (InvalidDocument == NULL) {
goto cleanup;
}

if (evalue && PyErr_GivenExceptionMatches(etype, InvalidDocument)) {
PyObject *msg = PyObject_Str(evalue);
msg = PyObject_Str(evalue);
if (msg) {
// Prepend doc to the existing message
PyObject *dict_str = PyObject_Str(dict);
if (dict_str == NULL) {
goto cleanup;
}
const char * dict_str_utf8 = PyUnicode_AsUTF8(dict_str);
if (dict_str_utf8 == NULL) {
goto cleanup;
}
const char * msg_utf8 = PyUnicode_AsUTF8(msg);
if (msg_utf8 == NULL) {
goto cleanup;
}
PyObject *new_msg = PyUnicode_FromFormat("Invalid document %s | %s", dict_str_utf8, msg_utf8);
new_msg = PyUnicode_FromFormat("Invalid document: %s", msg_utf8);
if (new_msg == NULL) {
goto cleanup;
}
// Add doc to the error instance as a property.
new_evalue = PyObject_CallFunctionObjArgs(InvalidDocument, new_msg, dict, NULL);
Py_DECREF(evalue);
Py_DECREF(etype);
etype = InvalidDocument;
InvalidDocument = NULL;
if (new_msg) {
evalue = new_msg;
if (new_evalue) {
evalue = new_evalue;
new_evalue = NULL;
} else {
evalue = msg;
msg = NULL;
}
}
PyErr_NormalizeException(&etype, &evalue, &etrace);
Expand All @@ -1689,7 +1687,7 @@ void handle_invalid_doc_error(PyObject* dict) {
PyErr_Restore(etype, evalue, etrace);
Py_XDECREF(msg);
Py_XDECREF(InvalidDocument);
Py_XDECREF(dict_str);
Py_XDECREF(new_evalue);
Py_XDECREF(new_msg);
}

Expand Down
13 changes: 13 additions & 0 deletions bson/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
"""Exceptions raised by the BSON package."""
from __future__ import annotations

from typing import Any, Optional


class BSONError(Exception):
"""Base class for all BSON exceptions."""
Expand All @@ -31,6 +33,17 @@ class InvalidStringData(BSONError):
class InvalidDocument(BSONError):
"""Raised when trying to create a BSON object from an invalid document."""

def __init__(self, message: str, document: Optional[Any] = None) -> None:
super().__init__(message)
self._document = document

@property
def document(self) -> Any:
"""The invalid document that caused the error.

..versionadded:: 4.16"""
return self._document
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should backport PYTHON-5449 since it has api changes. Is it possible to only backport the memory leak fix?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps, it would have to be basically a new PR.



class InvalidId(BSONError):
"""Raised when trying to create an ObjectId from invalid data."""
18 changes: 18 additions & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
Changelog
=========

Changes in Version 4.15.2 (2025/XX/YY)
--------------------------------------

Version 4.15.3 is a bug fix release.

- Removed invalid documents from :class:`bson.errors.InvalidDocument` error messages as
doing so may leak sensitive user data.
Instead, invalid documents are stored in :attr:`bson.errors.InvalidDocument.document`.

Issues Resolved
...............

See the `PyMongo 4.15.3 release notes in JIRA`_ for the list of resolved issues
in this release.

.. _PyMongo 4.15.3 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=47293

Changes in Version 4.15.2 (2025/10/01)
--------------------------------------

Expand All @@ -16,6 +33,7 @@ in this release.

.. _PyMongo 4.15.2 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=47186


Changes in Version 4.15.1 (2025/09/16)
--------------------------------------

Expand Down
15 changes: 11 additions & 4 deletions test/test_bson.py
Original file line number Diff line number Diff line change
Expand Up @@ -1163,7 +1163,7 @@ def __repr__(self):
):
encode({"t": Wrapper(1)})

def test_doc_in_invalid_document_error_message(self):
def test_doc_in_invalid_document_error_as_property(self):
class Wrapper:
def __init__(self, val):
self.val = val
Expand All @@ -1173,10 +1173,11 @@ def __repr__(self):

self.assertEqual("1", repr(Wrapper(1)))
doc = {"t": Wrapper(1)}
with self.assertRaisesRegex(InvalidDocument, f"Invalid document {doc}"):
with self.assertRaisesRegex(InvalidDocument, "Invalid document:") as cm:
encode(doc)
self.assertEqual(cm.exception.document, doc)

def test_doc_in_invalid_document_error_message_mapping(self):
def test_doc_in_invalid_document_error_as_property_mapping(self):
class MyMapping(abc.Mapping):
def keys(self):
return ["t"]
Expand All @@ -1192,6 +1193,11 @@ def __len__(self):
def __iter__(self):
return iter(["t"])

def __eq__(self, other):
if isinstance(other, MyMapping):
return True
return False

class Wrapper:
def __init__(self, val):
self.val = val
Expand All @@ -1201,8 +1207,9 @@ def __repr__(self):

self.assertEqual("1", repr(Wrapper(1)))
doc = MyMapping()
with self.assertRaisesRegex(InvalidDocument, f"Invalid document {doc}"):
with self.assertRaisesRegex(InvalidDocument, "Invalid document:") as cm:
encode(doc)
self.assertEqual(cm.exception.document, doc)


class TestCodecOptions(unittest.TestCase):
Expand Down
Loading