diff --git a/bson/__init__.py b/bson/__init__.py index b655e30c2c..6b2ba293a6 100644 --- a/bson/__init__.py +++ b/bson/__init__.py @@ -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 diff --git a/bson/_cbsonmodule.c b/bson/_cbsonmodule.c index be91e41734..7d184641c5 100644 --- a/bson/_cbsonmodule.c +++ b/bson/_cbsonmodule.c @@ -1645,11 +1645,11 @@ 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) { @@ -1657,30 +1657,28 @@ void handle_invalid_doc_error(PyObject* dict) { } 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); @@ -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); } diff --git a/bson/errors.py b/bson/errors.py index a3699e704c..ffc117f7ac 100644 --- a/bson/errors.py +++ b/bson/errors.py @@ -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.""" @@ -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 + class InvalidId(BSONError): """Raised when trying to create an ObjectId from invalid data.""" diff --git a/doc/changelog.rst b/doc/changelog.rst index 9ca1d47e98..99d84cba78 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -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) -------------------------------------- @@ -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) -------------------------------------- diff --git a/test/test_bson.py b/test/test_bson.py index e4cf85c46c..f792db1e89 100644 --- a/test/test_bson.py +++ b/test/test_bson.py @@ -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 @@ -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"] @@ -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 @@ -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):