From patchwork Thu Nov 5 07:12:41 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Yogita Urade X-Patchwork-Id: 102997 Received: from rcdn-iport-2.cisco.com (rcdn-iport-2.cisco.com [173.37.86.73]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.29109.1793862764845610971 for ; Wed, 04 Nov 2026 23:12:45 -0800 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: message contains an insecure body length tag" header.i=@cisco.com header.s=iport01 header.b=JGwollpN; spf=pass (domain: cisco.com, ip: 173.37.86.73, mailfrom: yurade@cisco.com) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cisco.com; i=@cisco.com; l=31719; q=dns/txt; s=iport01; t=1793862764; x=1795072364; h=from:to:subject:date:message-id:mime-version: content-transfer-encoding; bh=4chZ8BZtTztc5vvPdplcmeRY/6oYl9JnnheJbPXkoOY=; b=JGwollpNpiNmDaxa2tDNDWz5QUhu9VbxcD85cplUrUSMX+kVaCd2XNbx OHmK4nH6TQ77cohJ51SEDLLkygupOPOwfcgnYjsvt1Mc6uAowjUMW7AQq 4oc5WchD3bzOSD2QFYr1qxXLDOMkPJ6ONHSWGXHRSce1wws3+2VFxt2GD BLw8XJLDhgp6Pw/L0wI5ltAT5av29THj8Ri3//SOJLNflrL7JUH3RBiin hy5LP1IoYx5/RSLD4DNT//LEhGSHcb02DgKu7z3cHaV64L0wBOLlJR/hu w6m+SICVTC0aqYRmXyzyKCZxewlaxDm8KLhSACwJsYdyg18jfnGmz6asq Q==; X-CSE-ConnectionGUID: JlFPcl18SKWJ2nifYUn4Xg== X-CSE-MsgGUID: 1CR+2U5VQnSHkgsr5/0JRQ== X-IPAS-Result: A0BFAgDDKuxq/4//Ja1aglmCV3VgQ0mUKoE1bJ4kgX4PAQEBD0QNBAEBkyQCJjQJDgECBAMCAwEBAQEBAQEBAQEBAQoBAQUBAQECAQcFgQ4Thk8NkBIBIA0LARgBWQMBAlojIYMCAYJ0AxG7bRs3gXkzgQGDKQExBYEe2UuBaAELFAGBOIVAiCNdGAFEhDknG4FJRIR+gQWBXAKBOBCGXgSCDYEhgVoeZYwVAoFShnFIgR4DWSwBVRMNCgsHBYFoAzUSKhVuMh2BIz4XgQcbBgWBHYEegRKBB12BKyheIYEIgXYCglGCAAIBSUEOB0VTCSVDEkckIggSCQETGiwKgRo1PAkmPxUMZAsaBWEIAg0McjkQMB4kLwUcJkAbChgYDUgRLDcVGQQ+bgeSL4JkJgcOVQEqASsEexIPXT0DDTCSaQE5j2SCIYE1n1yEKowklToaM4QElBmSVAuYfYJZizGVYXCEaYFoPEaBEzMaCBsVO4JnCUoZD44uAQoLg2CFE8lIJjICCTIBAQcCBw4DC4FokAGBHWABAQ IronPort-Data: A9a23:vai7NKI06UrAguSjFE+Rh5QlxSXFcZb7ZxGr2PjKsXjdYENS0WdUn zZKWGrXM6rba2P0c4onaori90wBscLVzYRiQVcd+CA2RRqmiyZq6fd1j6vUF3nPRiEWZBs/t 63yUvGZcoZpCCea+Uf1WlTYhSEU/bmSQbbhA/LzNCl0RAt1IA8skhsLd9QR2uaEuvDnRVnS0 T/Oi5eHYgH8gmYoajh8B5+r8XuDgtyj4Fv0gXRmDRx7lAe2v2UYCpsZOZawIxPQKqFIHvS3T vr017qw+GXU5X8FUrtJRZ6iLyXm6paLVeS/oiI+t5qK23CulQRuukoPD8fwXG8M49m/c3+d/ /0W3XC4YV9B0qQhA43xWTEAe811FfUuFLMqvRFTvOTLp3AqfUcAzN1MCG8ONolBqt1UPkMU8 /0WGQ0LMiCM0rfeLLKTEoGAh+w5J8XteYdasXZ6wHSBU7AtQIvIROPB4towMDUY358VW62BI ZBENHw2ME+ojx5nYj/7DLovgf2hinTXeDxDo1XTrq0yi4TW5FEqiOKyYIKPJLRmQ+0MtXzb/ F+bwV3/OTEZO8eFlH3d0EiV07qncSTTHdh6+KeD3vlyjVuew2YeBBEbWR63rOe0jma6WslDM AoT4icooK04+UCnQ9W7WAe3yENopTYGUNZWVul/4waXx++MvUCSB3MPSXhKb9lOWNIKeAHGH 2Shx7vBbQGDepXMIZ5B3t94dQ+PBBU= IronPort-HdrOrdr: A9a23:Xqvm4aP20t5OUMBcTvGjsMiBIKoaSvp037BN7TESdfU7SKKlfq yV8cjztiWE6wr5JktApTnoAsDpKhnhHPVOjrX5U43PYOCfgguVBbAny5f+yDv9HCC73Otc2a B8N5VaMrTLfD1HZQKQ2njeLz7mq+P3lJyVuQ== X-Talos-CUID: 9a23:DhjkD2jAG7X8Ua1/OndJMVaiSzJuUHnz9HzUeh+DKllLEruUR26t/v9UnJ87 X-Talos-MUID: 9a23:wrVYhg1lIx3ZDqmwAyqQqk8cFzUjuo+USxowzKs/vfKkNiNrIWra1BaNe9py X-IronPort-Anti-Spam-Filtered: true X-IronPort-AV: E=Sophos;i="6.27,196,1787011200"; d="scan'208";a="530278026" Received: from rcdn-l-core-06.cisco.com ([173.37.255.143]) by rcdn-iport-2.cisco.com with ESMTP/TLS/TLS_AES_256_GCM_SHA384; 05 Nov 2026 07:12:43 +0000 Received: from sjc-ads-7310.cisco.com (sjc-ads-7310.cisco.com [10.30.220.95]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "ciscoit-managed-infra-smtp-auth.cisco.com", Issuer "Internal Private TLS SubCA" (verified OK)) by rcdn-l-core-06.cisco.com (Postfix) with ESMTPS id 9FFD718000385 for ; Thu, 5 Nov 2026 07:12:43 +0000 (GMT) Received: by sjc-ads-7310.cisco.com (Postfix, from userid 1889728) id 458D2CC12A6; Wed, 4 Nov 2026 23:12:43 -0800 (PST) From: Yogita Urade To: openembedded-devel@lists.openembedded.org Subject: [oe][meta-python][wrynose][PATCH 1/2] python3-pymongo: Fix CVE-2026-88029 Date: Wed, 4 Nov 2026 23:12:41 -0800 Message-Id: <20261105071242.2090700-1-yurade@cisco.com> X-Mailer: git-send-email 2.35.6 MIME-Version: 1.0 X-Outbound-Client-TLS: VERIFIED;sjc-ads-7310.cisco.com [10.30.220.95];TLSv1.3;TLS_AES_256_GCM_SHA384;256;ciscoit-managed-infra-smtp-auth.cisco.com X-Outbound-SMTP-Client: 10.30.220.95, sjc-ads-7310.cisco.com X-Outbound-Node: rcdn-l-core-06.cisco.com List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by lin-tor-korg-lkml-alma10-1 with HTTPS for ; Thu, 05 Nov 2026 07:12:50 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/131874 This patch applies the upstream fix as referenced in [2], using the commit shown in [1]. [1] https://github.com/mongodb/mongo-python-driver/commit/fa676586ba4b399168a4d1d41c30dc2166cc44fd [2] https://jira.mongodb.org/browse/PYTHON-5994 Signed-off-by: Yogita Urade --- .../python3-pymongo/CVE-2026-88029.patch | 1255 +++++++++++++++++ .../python/python3-pymongo_4.16.0.bb | 2 + 2 files changed, 1257 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-pymongo/CVE-2026-88029.patch diff --git a/meta-python/recipes-devtools/python/python3-pymongo/CVE-2026-88029.patch b/meta-python/recipes-devtools/python/python3-pymongo/CVE-2026-88029.patch new file mode 100644 index 0000000000..edb5683bd0 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-pymongo/CVE-2026-88029.patch @@ -0,0 +1,1255 @@ +From fa676586ba4b399168a4d1d41c30dc2166cc44fd Mon Sep 17 00:00:00 2001 +From: Jeffrey 'Alex' Clark +Date: Thu, 10 Sep 2026 10:12:37 -0400 +Subject: [PATCH] PYTHON-5994 Use exact match for file ID in GridFS delete + methods (#3046) + +Co-authored-by: Iris <58442094+sleepyStick@users.noreply.github.com> + +CVE: CVE-2026-88029 +Upstream-Status: Backport [https://github.com/mongodb/mongo-python-driver/commit/fa676586ba4b399168a4d1d41c30dc2166cc44fd] + +(cherry picked from commit fa676586ba4b399168a4d1d41c30dc2166cc44fd) +Signed-off-by: Yogita Urade +--- + gridfs/asynchronous/grid_file.py | 36 +- + gridfs/grid_file_shared.py | 4 +- + gridfs/synchronous/grid_file.py | 34 +- + test/asynchronous/test_gridfs_bucket.py | 30 + + test/gridfs/queries-use-eq.json | 528 ++++++++++++++++++ + .../unified/gridfs-download-serverErrors.json | 100 +++- + .../unified/gridfs-download.json | 24 +- + test/test_gridfs_bucket.py | 28 + + 8 files changed, 728 insertions(+), 56 deletions(-) + +diff --git a/gridfs/asynchronous/grid_file.py b/gridfs/asynchronous/grid_file.py +index e512f796..bd5d75aa 100644 +--- a/gridfs/asynchronous/grid_file.py ++++ b/gridfs/asynchronous/grid_file.py +@@ -282,8 +282,8 @@ class AsyncGridFS: + ``delete`` no longer ensures indexes. + """ + _disallow_transactions(session) +- await self._files.delete_one({"_id": file_id}, session=session) +- await self._chunks.delete_many({"files_id": file_id}, session=session) ++ await self._files.delete_one({"_id": {"$eq": file_id}}, session=session) ++ await self._chunks.delete_many({"files_id": {"$eq": file_id}}, session=session) + + async def list(self, session: Optional[AsyncClientSession] = None) -> list[str]: + """List the names of all files stored in this instance of +@@ -339,7 +339,7 @@ class AsyncGridFS: + Added ``session`` parameter. + """ + if filter is not None and not isinstance(filter, abc.Mapping): +- filter = {"_id": filter} ++ filter = {"_id": {"$eq": filter}} + + _disallow_transactions(session) + async for f in self.find(filter, *args, session=session, **kwargs): +@@ -453,6 +453,8 @@ class AsyncGridFS: + if kwargs: + f = await self._files.find_one(kwargs, ["_id"], session=session) + else: ++ if document_or_id is not None and not isinstance(document_or_id, abc.Mapping): ++ document_or_id = {"_id": {"$eq": document_or_id}} + f = await self._files.find_one(document_or_id, ["_id"], session=session) + + return f is not None +@@ -829,8 +831,8 @@ class AsyncGridFSBucket: + Added ``session`` parameter. + """ + _disallow_transactions(session) +- res = await self._files.delete_one({"_id": file_id}, session=session) +- await self._chunks.delete_many({"files_id": file_id}, session=session) ++ res = await self._files.delete_one({"_id": {"$eq": file_id}}, session=session) ++ await self._chunks.delete_many({"files_id": {"$eq": file_id}}, session=session) + if not res.deleted_count: + raise NoFile("no file could be deleted because none matched %s" % file_id) + +@@ -1042,7 +1044,7 @@ class AsyncGridFSBucket: + """ + _disallow_transactions(session) + result = await self._files.update_one( +- {"_id": file_id}, {"$set": {"filename": new_filename}}, session=session ++ {"_id": {"$eq": file_id}}, {"$set": {"filename": new_filename}}, session=session + ) + if not result.matched_count: + raise NoFile( +@@ -1197,8 +1199,12 @@ class AsyncGridIn: + + async def abort(self) -> None: + """Remove all chunks/files that may have been uploaded and close.""" +- await self._coll.chunks.delete_many({"files_id": self._file["_id"]}, session=self._session) +- await self._coll.files.delete_one({"_id": self._file["_id"]}, session=self._session) ++ await self._coll.chunks.delete_many( ++ {"files_id": {"$eq": self._file["_id"]}}, session=self._session ++ ) ++ await self._coll.files.delete_one( ++ {"_id": {"$eq": self._file["_id"]}}, session=self._session ++ ) + object.__setattr__(self, "_closed", True) + + @property +@@ -1247,7 +1253,9 @@ class AsyncGridIn: + self._file[name] = value + if self._closed: + if _IS_SYNC: +- self._coll.files.update_one({"_id": self._file["_id"]}, {"$set": {name: value}}) ++ self._coll.files.update_one( ++ {"_id": {"$eq": self._file["_id"]}}, {"$set": {name: value}} ++ ) + else: + raise AttributeError( + "AsyncGridIn does not support __setattr__ after being closed(). Set the attribute before closing the file or use AsyncGridIn.set() instead" +@@ -1256,7 +1264,9 @@ class AsyncGridIn: + async def set(self, name: str, value: Any) -> None: + self._file[name] = value + if self._closed: +- await self._coll.files.update_one({"_id": self._file["_id"]}, {"$set": {name: value}}) ++ await self._coll.files.update_one( ++ {"_id": {"$eq": self._file["_id"]}}, {"$set": {name: value}} ++ ) + + async def _flush_data(self, data: Any, force: bool = False) -> None: + """Flush `data` to a chunk.""" +@@ -1582,7 +1592,9 @@ class AsyncGridOut(GRIDOUT_BASE_CLASS): # type: ignore + async def open(self) -> None: + if not self._file: + _disallow_transactions(self._session) +- self._file = await self._files.find_one({"_id": self._file_id}, session=self._session) ++ self._file = await self._files.find_one( ++ {"_id": {"$eq": self._file_id}}, session=self._session ++ ) + if not self._file: + raise NoFile( + f"no file in gridfs collection {self._files!r} with _id {self._file_id!r}" +@@ -1853,7 +1865,7 @@ class _AsyncGridOutChunkIterator: + return self + + def _create_cursor(self) -> None: +- filter = {"files_id": self._id} ++ filter = {"files_id": {"$eq": self._id}} + if self._next_chunk > 0: + filter["n"] = {"$gte": self._next_chunk} + _disallow_transactions(self._session) +diff --git a/gridfs/grid_file_shared.py b/gridfs/grid_file_shared.py +index 79a0ad7f..79d863c8 100644 +--- a/gridfs/grid_file_shared.py ++++ b/gridfs/grid_file_shared.py +@@ -122,7 +122,9 @@ def _grid_in_property( + if warn_str: + warnings.warn(warn_str, stacklevel=2, category=DeprecationWarning) + if self._closed: +- self._coll.files.update_one({"_id": self._file["_id"]}, {"$set": {field_name: value}}) ++ self._coll.files.update_one( ++ {"_id": {"$eq": self._file["_id"]}}, {"$set": {field_name: value}} ++ ) + self._file[field_name] = value + + if read_only: +diff --git a/gridfs/synchronous/grid_file.py b/gridfs/synchronous/grid_file.py +index 70a4f807..a6cc6b50 100644 +--- a/gridfs/synchronous/grid_file.py ++++ b/gridfs/synchronous/grid_file.py +@@ -282,8 +282,8 @@ class GridFS: + ``delete`` no longer ensures indexes. + """ + _disallow_transactions(session) +- self._files.delete_one({"_id": file_id}, session=session) +- self._chunks.delete_many({"files_id": file_id}, session=session) ++ self._files.delete_one({"_id": {"$eq": file_id}}, session=session) ++ self._chunks.delete_many({"files_id": {"$eq": file_id}}, session=session) + + def list(self, session: Optional[ClientSession] = None) -> list[str]: + """List the names of all files stored in this instance of +@@ -337,7 +337,7 @@ class GridFS: + Added ``session`` parameter. + """ + if filter is not None and not isinstance(filter, abc.Mapping): +- filter = {"_id": filter} ++ filter = {"_id": {"$eq": filter}} + + _disallow_transactions(session) + for f in self.find(filter, *args, session=session, **kwargs): +@@ -451,6 +451,8 @@ class GridFS: + if kwargs: + f = self._files.find_one(kwargs, ["_id"], session=session) + else: ++ if document_or_id is not None and not isinstance(document_or_id, abc.Mapping): ++ document_or_id = {"_id": {"$eq": document_or_id}} + f = self._files.find_one(document_or_id, ["_id"], session=session) + + return f is not None +@@ -825,8 +827,8 @@ class GridFSBucket: + Added ``session`` parameter. + """ + _disallow_transactions(session) +- res = self._files.delete_one({"_id": file_id}, session=session) +- self._chunks.delete_many({"files_id": file_id}, session=session) ++ res = self._files.delete_one({"_id": {"$eq": file_id}}, session=session) ++ self._chunks.delete_many({"files_id": {"$eq": file_id}}, session=session) + if not res.deleted_count: + raise NoFile("no file could be deleted because none matched %s" % file_id) + +@@ -1034,7 +1036,7 @@ class GridFSBucket: + """ + _disallow_transactions(session) + result = self._files.update_one( +- {"_id": file_id}, {"$set": {"filename": new_filename}}, session=session ++ {"_id": {"$eq": file_id}}, {"$set": {"filename": new_filename}}, session=session + ) + if not result.matched_count: + raise NoFile( +@@ -1185,8 +1187,10 @@ class GridIn: + + def abort(self) -> None: + """Remove all chunks/files that may have been uploaded and close.""" +- self._coll.chunks.delete_many({"files_id": self._file["_id"]}, session=self._session) +- self._coll.files.delete_one({"_id": self._file["_id"]}, session=self._session) ++ self._coll.chunks.delete_many( ++ {"files_id": {"$eq": self._file["_id"]}}, session=self._session ++ ) ++ self._coll.files.delete_one({"_id": {"$eq": self._file["_id"]}}, session=self._session) + object.__setattr__(self, "_closed", True) + + @property +@@ -1235,7 +1239,9 @@ class GridIn: + self._file[name] = value + if self._closed: + if _IS_SYNC: +- self._coll.files.update_one({"_id": self._file["_id"]}, {"$set": {name: value}}) ++ self._coll.files.update_one( ++ {"_id": {"$eq": self._file["_id"]}}, {"$set": {name: value}} ++ ) + else: + raise AttributeError( + "GridIn does not support __setattr__ after being closed(). Set the attribute before closing the file or use GridIn.set() instead" +@@ -1244,7 +1250,9 @@ class GridIn: + def set(self, name: str, value: Any) -> None: + self._file[name] = value + if self._closed: +- self._coll.files.update_one({"_id": self._file["_id"]}, {"$set": {name: value}}) ++ self._coll.files.update_one( ++ {"_id": {"$eq": self._file["_id"]}}, {"$set": {name: value}} ++ ) + + def _flush_data(self, data: Any, force: bool = False) -> None: + """Flush `data` to a chunk.""" +@@ -1570,7 +1578,9 @@ class GridOut(GRIDOUT_BASE_CLASS): # type: ignore + def open(self) -> None: + if not self._file: + _disallow_transactions(self._session) +- self._file = self._files.find_one({"_id": self._file_id}, session=self._session) ++ self._file = self._files.find_one( ++ {"_id": {"$eq": self._file_id}}, session=self._session ++ ) + if not self._file: + raise NoFile( + f"no file in gridfs collection {self._files!r} with _id {self._file_id!r}" +@@ -1841,7 +1851,7 @@ class GridOutChunkIterator: + return self + + def _create_cursor(self) -> None: +- filter = {"files_id": self._id} ++ filter = {"files_id": {"$eq": self._id}} + if self._next_chunk > 0: + filter["n"] = {"$gte": self._next_chunk} + _disallow_transactions(self._session) +diff --git a/test/asynchronous/test_gridfs_bucket.py b/test/asynchronous/test_gridfs_bucket.py +index 6b3503d122..ac850f1063 100644 +--- a/test/asynchronous/test_gridfs_bucket.py ++++ b/test/asynchronous/test_gridfs_bucket.py +@@ -33,6 +33,7 @@ + import gridfs + from bson.binary import Binary + from bson.int64 import Int64 ++from bson.min_key import MinKey + from bson.objectid import ObjectId + from bson.son import SON + from gridfs.errors import CorruptGridFile, NoFile +@@ -42,10 +42,11 @@ + from pymongo.errors import ( + ConfigurationError, + NotPrimaryError, + ServerSelectionTimeoutError, + WriteConcernError, ++ WriteError, + ) + from pymongo.read_preferences import ReadPreference + + _IS_SYNC = False + +@@ -475,6 +477,34 @@ async def test_abort(self): + await gin.write(b"test4") + self.assertEqual(0, await self.db.fs.chunks.count_documents({"files_id": gin._id})) + ++ # Server versions older than 5.0 do not support document values with ++ # "$"-prefixed keys. ++ @async_client_context.require_version_min(5, 0) ++ async def test_abort_with_injected_file_id_does_not_delete_other_files(self): ++ bucket = self.fs ++ file1_bytes = b"\x11\x22\x33\x44" ++ await bucket.upload_from_stream("file1", file1_bytes) ++ ++ upload_stream = bucket.open_upload_stream_with_id( ++ {"$gt": MinKey()}, "file2", chunk_size_bytes=2 ++ ) ++ await upload_stream.write(b"\x55\x66\x77\x88") ++ try: ++ await upload_stream.abort() ++ except WriteError: ++ # Newer server versions reject an equality match on "_id" whose ++ # operand itself resembles a query operator, even when the ++ # driver safely wraps it in "$eq". The chunks are still cleaned ++ # up in that case; only the (never created) files document ++ # delete is rejected. ++ pass ++ ++ self.assertEqual( ++ file1_bytes, await (await bucket.open_download_stream_by_name("file1")).read() ++ ) ++ with self.assertRaises(NoFile): ++ await bucket.open_download_stream_by_name("file2") ++ + async def test_download_to_stream(self): + file1 = BytesIO(b"hello world") + # Test with one chunk. +diff --git a/test/gridfs/queries-use-eq.json b/test/gridfs/queries-use-eq.json +new file mode 100644 +index 0000000000..e068168b91 +--- /dev/null ++++ b/test/gridfs/queries-use-eq.json +@@ -0,0 +1,528 @@ ++{ ++ "description": "gridfs-queriesUseEq", ++ "schemaVersion": "1.1", ++ "createEntities": [ ++ { ++ "client": { ++ "id": "client0", ++ "observeEvents": [ ++ "commandStartedEvent" ++ ] ++ } ++ }, ++ { ++ "database": { ++ "id": "database0", ++ "client": "client0", ++ "databaseName": "gridfs-tests" ++ } ++ }, ++ { ++ "bucket": { ++ "id": "bucket0", ++ "database": "database0" ++ } ++ }, ++ { ++ "collection": { ++ "id": "bucket0_files_collection", ++ "database": "database0", ++ "collectionName": "fs.files" ++ } ++ }, ++ { ++ "collection": { ++ "id": "bucket0_chunks_collection", ++ "database": "database0", ++ "collectionName": "fs.chunks" ++ } ++ } ++ ], ++ "initialData": [ ++ { ++ "collectionName": "fs.files", ++ "databaseName": "gridfs-tests", ++ "documents": [ ++ { ++ "_id": { ++ "$oid": "000000000000000000000001" ++ }, ++ "length": 2, ++ "chunkSize": 4, ++ "uploadDate": { ++ "$date": "1970-01-01T00:00:00.000Z" ++ }, ++ "filename": "file-1" ++ }, ++ { ++ "_id": { ++ "$oid": "000000000000000000000002" ++ }, ++ "length": 0, ++ "chunkSize": 4, ++ "uploadDate": { ++ "$date": "1970-01-01T00:00:00.000Z" ++ }, ++ "filename": "file-2" ++ } ++ ] ++ }, ++ { ++ "collectionName": "fs.chunks", ++ "databaseName": "gridfs-tests", ++ "documents": [ ++ { ++ "_id": { ++ "$oid": "000000000000000000000011" ++ }, ++ "files_id": { ++ "$oid": "000000000000000000000001" ++ }, ++ "n": 0, ++ "data": { ++ "$binary": { ++ "base64": "ESI=", ++ "subType": "00" ++ } ++ } ++ } ++ ] ++ } ++ ], ++ "_yamlAnchors": { ++ "injectedId": { ++ "$gt": { ++ "$minKey": 1 ++ } ++ }, ++ "unchangedOutcome": [ ++ { ++ "collectionName": "fs.files", ++ "databaseName": "gridfs-tests", ++ "documents": [ ++ { ++ "_id": { ++ "$oid": "000000000000000000000001" ++ }, ++ "length": 2, ++ "chunkSize": 4, ++ "uploadDate": { ++ "$date": "1970-01-01T00:00:00.000Z" ++ }, ++ "filename": "file-1" ++ }, ++ { ++ "_id": { ++ "$oid": "000000000000000000000002" ++ }, ++ "length": 0, ++ "chunkSize": 4, ++ "uploadDate": { ++ "$date": "1970-01-01T00:00:00.000Z" ++ }, ++ "filename": "file-2" ++ } ++ ] ++ }, ++ { ++ "collectionName": "fs.chunks", ++ "databaseName": "gridfs-tests", ++ "documents": [ ++ { ++ "_id": { ++ "$oid": "000000000000000000000011" ++ }, ++ "files_id": { ++ "$oid": "000000000000000000000001" ++ }, ++ "n": 0, ++ "data": { ++ "$binary": { ++ "base64": "ESI=", ++ "subType": "00" ++ } ++ } ++ } ++ ] ++ } ++ ] ++ }, ++ "tests": [ ++ { ++ "description": "download uses $eq to query the files and chunks collections", ++ "operations": [ ++ { ++ "name": "download", ++ "object": "bucket0", ++ "arguments": { ++ "id": { ++ "$oid": "000000000000000000000001" ++ } ++ }, ++ "expectResult": { ++ "$$matchesHexBytes": "1122" ++ } ++ } ++ ], ++ "expectEvents": [ ++ { ++ "client": "client0", ++ "events": [ ++ { ++ "commandStartedEvent": { ++ "command": { ++ "find": "fs.files", ++ "filter": { ++ "_id": { ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } ++ } ++ } ++ } ++ } ++ }, ++ { ++ "commandStartedEvent": { ++ "command": { ++ "find": "fs.chunks", ++ "filter": { ++ "files_id": { ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } ++ } ++ } ++ } ++ } ++ } ++ ] ++ } ++ ] ++ }, ++ { ++ "description": "delete uses $eq to delete the files collection document and chunks", ++ "operations": [ ++ { ++ "name": "delete", ++ "object": "bucket0", ++ "arguments": { ++ "id": { ++ "$oid": "000000000000000000000001" ++ } ++ } ++ } ++ ], ++ "expectEvents": [ ++ { ++ "client": "client0", ++ "events": [ ++ { ++ "commandStartedEvent": { ++ "command": { ++ "delete": "fs.files", ++ "deletes": [ ++ { ++ "q": { ++ "_id": { ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } ++ } ++ }, ++ "limit": 1 ++ } ++ ] ++ } ++ } ++ }, ++ { ++ "commandStartedEvent": { ++ "command": { ++ "delete": "fs.chunks", ++ "deletes": [ ++ { ++ "q": { ++ "files_id": { ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } ++ } ++ }, ++ "limit": 0 ++ } ++ ] ++ } ++ } ++ } ++ ] ++ } ++ ], ++ "outcome": [ ++ { ++ "collectionName": "fs.files", ++ "databaseName": "gridfs-tests", ++ "documents": [ ++ { ++ "_id": { ++ "$oid": "000000000000000000000002" ++ }, ++ "length": 0, ++ "chunkSize": 4, ++ "uploadDate": { ++ "$date": "1970-01-01T00:00:00.000Z" ++ }, ++ "filename": "file-2" ++ } ++ ] ++ }, ++ { ++ "collectionName": "fs.chunks", ++ "databaseName": "gridfs-tests", ++ "documents": [] ++ } ++ ] ++ }, ++ { ++ "description": "rename uses $eq to update the files collection document", ++ "operations": [ ++ { ++ "name": "rename", ++ "object": "bucket0", ++ "arguments": { ++ "id": { ++ "$oid": "000000000000000000000001" ++ }, ++ "newFilename": "file-1-renamed" ++ } ++ } ++ ], ++ "expectEvents": [ ++ { ++ "client": "client0", ++ "events": [ ++ { ++ "commandStartedEvent": { ++ "command": { ++ "update": "fs.files", ++ "updates": [ ++ { ++ "q": { ++ "_id": { ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } ++ } ++ }, ++ "u": { ++ "$set": { ++ "filename": "file-1-renamed" ++ } ++ }, ++ "multi": { ++ "$$unsetOrMatches": false ++ }, ++ "upsert": { ++ "$$unsetOrMatches": false ++ } ++ } ++ ] ++ } ++ } ++ } ++ ] ++ } ++ ], ++ "outcome": [ ++ { ++ "collectionName": "fs.files", ++ "databaseName": "gridfs-tests", ++ "documents": [ ++ { ++ "_id": { ++ "$oid": "000000000000000000000001" ++ }, ++ "length": 2, ++ "chunkSize": 4, ++ "uploadDate": { ++ "$date": "1970-01-01T00:00:00.000Z" ++ }, ++ "filename": "file-1-renamed" ++ }, ++ { ++ "_id": { ++ "$oid": "000000000000000000000002" ++ }, ++ "length": 0, ++ "chunkSize": 4, ++ "uploadDate": { ++ "$date": "1970-01-01T00:00:00.000Z" ++ }, ++ "filename": "file-2" ++ } ++ ] ++ } ++ ] ++ }, ++ { ++ "description": "delete with a file id containing a query operator does not delete any files", ++ "operations": [ ++ { ++ "name": "delete", ++ "object": "bucket0", ++ "arguments": { ++ "id": { ++ "$gt": { ++ "$minKey": 1 ++ } ++ } ++ }, ++ "expectError": { ++ "isError": true ++ } ++ } ++ ], ++ "outcome": [ ++ { ++ "collectionName": "fs.files", ++ "databaseName": "gridfs-tests", ++ "documents": [ ++ { ++ "_id": { ++ "$oid": "000000000000000000000001" ++ }, ++ "length": 2, ++ "chunkSize": 4, ++ "uploadDate": { ++ "$date": "1970-01-01T00:00:00.000Z" ++ }, ++ "filename": "file-1" ++ }, ++ { ++ "_id": { ++ "$oid": "000000000000000000000002" ++ }, ++ "length": 0, ++ "chunkSize": 4, ++ "uploadDate": { ++ "$date": "1970-01-01T00:00:00.000Z" ++ }, ++ "filename": "file-2" ++ } ++ ] ++ }, ++ { ++ "collectionName": "fs.chunks", ++ "databaseName": "gridfs-tests", ++ "documents": [ ++ { ++ "_id": { ++ "$oid": "000000000000000000000011" ++ }, ++ "files_id": { ++ "$oid": "000000000000000000000001" ++ }, ++ "n": 0, ++ "data": { ++ "$binary": { ++ "base64": "ESI=", ++ "subType": "00" ++ } ++ } ++ } ++ ] ++ } ++ ] ++ }, ++ { ++ "description": "download with a file id containing a query operator does not return any files", ++ "operations": [ ++ { ++ "name": "download", ++ "object": "bucket0", ++ "arguments": { ++ "id": { ++ "$gt": { ++ "$minKey": 1 ++ } ++ } ++ }, ++ "expectError": { ++ "isClientError": true ++ } ++ } ++ ] ++ }, ++ { ++ "description": "rename with a file id containing a query operator does not rename any files", ++ "operations": [ ++ { ++ "name": "rename", ++ "object": "bucket0", ++ "arguments": { ++ "id": { ++ "$gt": { ++ "$minKey": 1 ++ } ++ }, ++ "newFilename": "injected" ++ }, ++ "expectError": { ++ "isError": true ++ } ++ } ++ ], ++ "outcome": [ ++ { ++ "collectionName": "fs.files", ++ "databaseName": "gridfs-tests", ++ "documents": [ ++ { ++ "_id": { ++ "$oid": "000000000000000000000001" ++ }, ++ "length": 2, ++ "chunkSize": 4, ++ "uploadDate": { ++ "$date": "1970-01-01T00:00:00.000Z" ++ }, ++ "filename": "file-1" ++ }, ++ { ++ "_id": { ++ "$oid": "000000000000000000000002" ++ }, ++ "length": 0, ++ "chunkSize": 4, ++ "uploadDate": { ++ "$date": "1970-01-01T00:00:00.000Z" ++ }, ++ "filename": "file-2" ++ } ++ ] ++ }, ++ { ++ "collectionName": "fs.chunks", ++ "databaseName": "gridfs-tests", ++ "documents": [ ++ { ++ "_id": { ++ "$oid": "000000000000000000000011" ++ }, ++ "files_id": { ++ "$oid": "000000000000000000000001" ++ }, ++ "n": 0, ++ "data": { ++ "$binary": { ++ "base64": "ESI=", ++ "subType": "00" ++ } ++ } ++ } ++ ] ++ } ++ ] ++ } ++ ] ++} +diff --git a/test/retryable_reads/unified/gridfs-download-serverErrors.json b/test/retryable_reads/unified/gridfs-download-serverErrors.json +index 5bb7eee0b2..dc87dadce4 100644 +--- a/test/retryable_reads/unified/gridfs-download-serverErrors.json ++++ b/test/retryable_reads/unified/gridfs-download-serverErrors.json +@@ -128,7 +128,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -141,7 +143,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -205,7 +209,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -218,7 +224,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -282,7 +290,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -295,7 +305,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -359,7 +371,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -372,7 +386,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -436,7 +452,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -449,7 +467,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -513,7 +533,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -526,7 +548,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -590,7 +614,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -603,7 +629,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -667,7 +695,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -680,7 +710,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -744,7 +776,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -757,7 +791,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -821,7 +857,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -834,7 +872,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -898,7 +938,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -911,7 +953,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -975,7 +1019,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -988,7 +1034,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -1077,7 +1125,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +diff --git a/test/retryable_reads/unified/gridfs-download.json b/test/retryable_reads/unified/gridfs-download.json +index 69fe8ff7c8..9b2f1d93d4 100644 +--- a/test/retryable_reads/unified/gridfs-download.json ++++ b/test/retryable_reads/unified/gridfs-download.json +@@ -109,7 +109,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -173,7 +175,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -186,7 +190,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -283,7 +289,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -339,7 +347,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +@@ -352,7 +362,9 @@ + "find": "fs.files", + "filter": { + "_id": { +- "$oid": "000000000000000000000001" ++ "$eq": { ++ "$oid": "000000000000000000000001" ++ } + } + } + }, +diff --git a/test/test_gridfs_bucket.py b/test/test_gridfs_bucket.py +index 4d1ca7b811..90b063a9b1 100644 +--- a/test/test_gridfs_bucket.py ++++ b/test/test_gridfs_bucket.py +@@ -33,6 +33,7 @@ + import gridfs + from bson.binary import Binary + from bson.int64 import Int64 ++from bson.min_key import MinKey + from bson.objectid import ObjectId + from bson.son import SON + from gridfs.errors import CorruptGridFile, NoFile +@@ -41,6 +42,7 @@ + NotPrimaryError, + ServerSelectionTimeoutError, + WriteConcernError, ++ WriteError, + ) + from pymongo.read_preferences import ReadPreference + from pymongo.synchronous.mongo_client import MongoClient +@@ -445,6 +447,32 @@ def test_abort(self): + gin.write(b"test4") + self.assertEqual(0, self.db.fs.chunks.count_documents({"files_id": gin._id})) + ++ # Server versions older than 5.0 do not support document values with ++ # "$"-prefixed keys. ++ @client_context.require_version_min(5, 0) ++ def test_abort_with_injected_file_id_does_not_delete_other_files(self): ++ bucket = self.fs ++ file1_bytes = b"\x11\x22\x33\x44" ++ bucket.upload_from_stream("file1", file1_bytes) ++ ++ upload_stream = bucket.open_upload_stream_with_id( ++ {"$gt": MinKey()}, "file2", chunk_size_bytes=2 ++ ) ++ upload_stream.write(b"\x55\x66\x77\x88") ++ try: ++ upload_stream.abort() ++ except WriteError: ++ # Newer server versions reject an equality match on "_id" whose ++ # operand itself resembles a query operator, even when the ++ # driver safely wraps it in "$eq". The chunks are still cleaned ++ # up in that case; only the (never created) files document ++ # delete is rejected. ++ pass ++ ++ self.assertEqual(file1_bytes, (bucket.open_download_stream_by_name("file1")).read()) ++ with self.assertRaises(NoFile): ++ bucket.open_download_stream_by_name("file2") ++ + def test_download_to_stream(self): + file1 = BytesIO(b"hello world") + # Test with one chunk. +-- +2.35.6 + diff --git a/meta-python/recipes-devtools/python/python3-pymongo_4.16.0.bb b/meta-python/recipes-devtools/python/python3-pymongo_4.16.0.bb index 75d0504d46..f3a4ae4ad1 100644 --- a/meta-python/recipes-devtools/python/python3-pymongo_4.16.0.bb +++ b/meta-python/recipes-devtools/python/python3-pymongo_4.16.0.bb @@ -8,6 +8,8 @@ HOMEPAGE = "https://github.com/mongodb/mongo-python-driver" LICENSE = "Apache-2.0" LIC_FILES_CHKSUM = "file://LICENSE;md5=86d3f3a95c324c9479bd8986968f4327" +SRC_URI += "file://CVE-2026-88029.patch" + SRC_URI[sha256sum] = "8ba8405065f6e258a6f872fe62d797a28f383a12178c7153c01ed04e845c600c" CVE_PRODUCT = "pymongo"