From dee2cf1d1d87fbd480818c6bd7b9711e120a05b8 Mon Sep 17 00:00:00 2001 From: Madan Kumar Date: Sun, 26 Jul 2026 05:41:57 +0530 Subject: [PATCH 1/3] PYTHON-XXXX Reject extra fields in JSON timestamps --- bson/json_util.py | 2 ++ doc/changelog.rst | 2 ++ doc/contributors.rst | 1 + test/test_json_util.py | 12 ++++++++++++ 4 files changed, 17 insertions(+) diff --git a/bson/json_util.py b/bson/json_util.py index 098a4b18ce..f01cd6cd19 100644 --- a/bson/json_util.py +++ b/bson/json_util.py @@ -789,6 +789,8 @@ def _parse_binary(doc: Any, json_options: JSONOptions) -> Union[Binary, uuid.UUI def _parse_timestamp(doc: Any, dummy0: Any) -> Timestamp: tsp = doc["$timestamp"] + if set(tsp) != {"t", "i"}: + raise TypeError(f'$timestamp must include exactly "t" and "i" components: {doc}') return Timestamp(tsp["t"], tsp["i"]) diff --git a/doc/changelog.rst b/doc/changelog.rst index c272714376..cda288c575 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -25,6 +25,8 @@ PyMongo 4.18 brings a number of changes including: the bytes remaining in the array now raises :class:`~bson.errors.InvalidBSON` instead of reading past the end of the buffer. +- Fixed :func:`bson.json_util.loads` to reject ``$timestamp`` values containing + fields other than ``t`` and ``i``. Changes in Version 4.17.0 (2026/04/20) -------------------------------------- diff --git a/doc/contributors.rst b/doc/contributors.rst index d36731159e..6c2cff68a3 100644 --- a/doc/contributors.rst +++ b/doc/contributors.rst @@ -109,3 +109,4 @@ The following is a list of people who have contributed to - Noah Stapp (NoahStapp) - Cal Jacobson (cj81499) - Sophia Yang (sophiayangDB) +- Madan Kumar (winklemad) diff --git a/test/test_json_util.py b/test/test_json_util.py index cd86456ab0..f1b781d3d9 100644 --- a/test/test_json_util.py +++ b/test/test_json_util.py @@ -405,6 +405,18 @@ def test_timestamp(self): self.assertEqual(dct, rtdct) self.assertEqual('{"ts": {"$timestamp": {"t": 4, "i": 13}}}', res) + def test_timestamp_with_invalid_fields(self): + invalid_values = [ + '{"t": 4, "i": 13, "extra": 1}', + '{"t": 4, "unexpected": 13}', + ] + for value in invalid_values: + with self.subTest(value=value): + with self.assertRaisesRegex( + TypeError, r'\$timestamp must include exactly "t" and "i" components' + ): + json_util.loads(f'{{"ts": {{"$timestamp": {value}}}}}') + def test_uuid_default(self): # Cannot directly encode native UUIDs with the default # uuid_representation. From a9d62ccbd4d54cf068de77e02e67811b1d230897 Mon Sep 17 00:00:00 2001 From: Madan Kumar Date: Mon, 27 Jul 2026 23:14:16 +0530 Subject: [PATCH 2/3] Reject extra $timestamp wrapper fields and guard non-document values --- bson/json_util.py | 4 ++++ test/test_json_util.py | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/bson/json_util.py b/bson/json_util.py index f01cd6cd19..e12e04ccb8 100644 --- a/bson/json_util.py +++ b/bson/json_util.py @@ -788,7 +788,11 @@ def _parse_binary(doc: Any, json_options: JSONOptions) -> Union[Binary, uuid.UUI def _parse_timestamp(doc: Any, dummy0: Any) -> Timestamp: + if len(doc) != 1: + raise TypeError(f"Bad $timestamp, extra field(s): {doc}") tsp = doc["$timestamp"] + if not isinstance(tsp, Mapping): + raise TypeError(f'$timestamp value must be a document with "t" and "i" components: {doc}') if set(tsp) != {"t", "i"}: raise TypeError(f'$timestamp must include exactly "t" and "i" components: {doc}') return Timestamp(tsp["t"], tsp["i"]) diff --git a/test/test_json_util.py b/test/test_json_util.py index f1b781d3d9..02d0b80ef1 100644 --- a/test/test_json_util.py +++ b/test/test_json_util.py @@ -417,6 +417,18 @@ def test_timestamp_with_invalid_fields(self): ): json_util.loads(f'{{"ts": {{"$timestamp": {value}}}}}') + def test_timestamp_with_extra_wrapper_fields(self): + with self.assertRaisesRegex(TypeError, r"Bad \$timestamp, extra field\(s\)"): + json_util.loads('{"ts": {"$timestamp": {"t": 4, "i": 13}, "extra": 1}}') + + def test_timestamp_with_non_document_value(self): + for value in ('["t", "i"]', '"ti"', "5"): + with self.subTest(value=value): + with self.assertRaisesRegex( + TypeError, r"\$timestamp value must be a document" + ): + json_util.loads(f'{{"ts": {{"$timestamp": {value}}}}}') + def test_uuid_default(self): # Cannot directly encode native UUIDs with the default # uuid_representation. From 55c0ececbb4791652181f6b7ac7056b52dd6d70f Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 27 Jul 2026 18:24:23 -0500 Subject: [PATCH 3/3] Apply ruff-format --- test/test_json_util.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/test_json_util.py b/test/test_json_util.py index 02d0b80ef1..ada04955c4 100644 --- a/test/test_json_util.py +++ b/test/test_json_util.py @@ -424,9 +424,7 @@ def test_timestamp_with_extra_wrapper_fields(self): def test_timestamp_with_non_document_value(self): for value in ('["t", "i"]', '"ti"', "5"): with self.subTest(value=value): - with self.assertRaisesRegex( - TypeError, r"\$timestamp value must be a document" - ): + with self.assertRaisesRegex(TypeError, r"\$timestamp value must be a document"): json_util.loads(f'{{"ts": {{"$timestamp": {value}}}}}') def test_uuid_default(self):