Skip to content
Merged
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
18 changes: 13 additions & 5 deletions bson/buffer.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#define PY_SSIZE_T_CLEAN
#include "Python.h"

#include <limits.h>
#include <stdlib.h>
#include <string.h>

Expand Down Expand Up @@ -105,18 +106,25 @@ static int buffer_grow(buffer_t buffer, int min_length) {
* Return non-zero and sets MemoryError on allocation failure.
* Return non-zero and sets ValueError if `size` would exceed 2GiB. */
static int buffer_assure_space(buffer_t buffer, int size) {
int new_size = buffer->position + size;
/* Check for overflow. */
if (new_size < buffer->position) {
long long new_size;
if (size < 0) {
PyErr_SetString(PyExc_ValueError,
"Document would overflow BSON size limit");
return 1;
}

if (new_size <= buffer->size) {
/* Compute in a wider type so the addition cannot overflow `int`. */
new_size = (long long)buffer->position + (long long)size;
if (new_size > INT_MAX) {
PyErr_SetString(PyExc_ValueError,
"Document would overflow BSON size limit");
return 1;
}

if ((int)new_size <= buffer->size) {
return 0;
}
return buffer_grow(buffer, new_size);
return buffer_grow(buffer, (int)new_size);
}

/* Save `size` bytes from the current position in `buffer` (and grow if needed).
Expand Down
20 changes: 19 additions & 1 deletion doc/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
Changelog
=========

Changes in Version 4.18.2 (2026/09/24)
--------------------------------------

Version 4.18.2 is a bug fix release.

- Hardened the bson buffer size guard against signed integer overflow.
- Fixed connection string parsing to percent-decode each host individually.
- Client-side field level encryption now rejects a KMS endpoint ending in
``.sock``.

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

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

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

Changes in Version 4.18.1 (2026/09/10)
--------------------------------------

Expand All @@ -9,7 +27,7 @@ Version 4.18.1 is a bug fix release.
- Use an exact match for the file ID in GridFS delete methods
(`CVE-2026-88029`_).

.. CVE-2026-88029: https://www.cve.org/CVERecord?id=CVE-2026-88029
.. _CVE-2026-88029: https://www.cve.org/CVERecord?id=CVE-2026-88029

Changes in Version 4.18.0 (2026/09/03)
--------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion pymongo/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import re
from typing import Union

__version__ = "4.18.2.dev0"
__version__ = "4.18.2"


def get_version_tuple(version: str) -> tuple[Union[int, str], ...]:
Expand Down
2 changes: 2 additions & 0 deletions pymongo/asynchronous/encryption.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ async def kms_request(self, kms_context: MongoCryptKmsContext) -> None:
ssl_context=ctx,
)
address = parse_host(endpoint, _HTTPS_PORT)
if address[0].endswith(".sock"):
raise ConfigurationError(f"Invalid KMS endpoint {endpoint!r}")
sleep_u = kms_context.usleep
if sleep_u:
sleep_sec = float(sleep_u) / 1e6
Expand Down
2 changes: 0 additions & 2 deletions pymongo/asynchronous/uri_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from __future__ import annotations

from typing import Any, Optional
from urllib.parse import unquote_plus

from pymongo.asynchronous.srv_resolver import _SrvResolver
from pymongo.common import SRV_SERVICE_NAME, _CaseInsensitiveDictionary
Expand Down Expand Up @@ -159,7 +158,6 @@ async def _parse_srv(
else:
hosts = host_part

hosts = unquote_plus(hosts)
srv_max_hosts = srv_max_hosts or options.get("srvMaxHosts")
srv_allowed_hosts_suffix = srv_allowed_hosts_suffix or options.get("srvAllowedHostsSuffix")
if is_srv:
Expand Down
2 changes: 2 additions & 0 deletions pymongo/synchronous/encryption.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ def kms_request(self, kms_context: MongoCryptKmsContext) -> None:
ssl_context=ctx,
)
address = parse_host(endpoint, _HTTPS_PORT)
if address[0].endswith(".sock"):
raise ConfigurationError(f"Invalid KMS endpoint {endpoint!r}")
sleep_u = kms_context.usleep
if sleep_u:
sleep_sec = float(sleep_u) / 1e6
Expand Down
2 changes: 0 additions & 2 deletions pymongo/synchronous/uri_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from __future__ import annotations

from typing import Any, Optional
from urllib.parse import unquote_plus

from pymongo.common import SRV_SERVICE_NAME, _CaseInsensitiveDictionary
from pymongo.errors import ConfigurationError, InvalidURI
Expand Down Expand Up @@ -159,7 +158,6 @@ def _parse_srv(
else:
hosts = host_part

hosts = unquote_plus(hosts)
srv_max_hosts = srv_max_hosts or options.get("srvMaxHosts")
srv_allowed_hosts_suffix = srv_allowed_hosts_suffix or options.get("srvAllowedHostsSuffix")
if is_srv:
Expand Down
18 changes: 15 additions & 3 deletions pymongo/uri_parser_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,10 +450,23 @@ def split_hosts(hosts: str, default_port: Optional[int] = DEFAULT_PORT) -> list[
if not entity:
raise ConfigurationError("Empty host (or extra comma in host list)")
port = default_port
# Unix socket entities don't have ports
node = entity
# Decoding happens per entity, after splitting on ",".
if entity.endswith(".sock"):
# Unix socket entities don't have ports. Socket paths are the
# only host identifiers permitted to contain reserved
# characters (e.g. "/") that require escaping.
node = unquote_plus(entity)
port = None
nodes.append(parse_host(entity, port))
elif entity.startswith("["):
# An IPv6 zone index is escaped as "%25" (RFC 6874).
node = entity.replace("%25", "%")
elif "%" in entity:
raise InvalidURI(
"Percent-encoding is only allowed in Unix domain socket paths "
f"and IPv6 zone indexes, not in hostnames: {entity}"
)
nodes.append(parse_host(node, port))
return nodes


Expand Down Expand Up @@ -576,7 +589,6 @@ def _validate_uri(
if "/" in hosts:
raise InvalidURI(f"Any '/' in a unix domain socket must be percent-encoded: {host_part}")

hosts = unquote_plus(hosts)
fqdn = None
srv_max_hosts = srv_max_hosts or options.get("srvMaxHosts")
if is_srv:
Expand Down
8 changes: 8 additions & 0 deletions test/asynchronous/test_encryption.py
Original file line number Diff line number Diff line change
Expand Up @@ -1299,6 +1299,14 @@ async def test_04_kmip_endpoint_invalid_port(self):
with self.assertRaisesRegex(EncryptionError, "localhost:12345"):
await self.client_encryption.create_data_key("kmip", master_key=master_key)

async def test_kmip_endpoint_unix_socket_rejected(self):
# PYTHON-5990: a masterKey.endpoint ending in ".sock" must not be
# treated as a Unix domain socket path. KMS endpoints must be a TCP
# host[:port].
master_key = {"keyId": "1", "endpoint": "example.sock"}
with self.assertRaisesRegex(EncryptionError, "Invalid KMS endpoint"):
await self.client_encryption.create_data_key("kmip", master_key=master_key)

@unittest.skipUnless(any(AWS_CREDS.values()), "AWS environment credentials are not set")
async def test_05_aws_endpoint_wrong_region(self):
master_key = {
Expand Down
8 changes: 8 additions & 0 deletions test/test_bson.py
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,14 @@ def test_overflow(self):
self.assertTrue(encode({"x": -9223372036854775808}))
self.assertRaises(OverflowError, encode, {"x": -9223372036854775809})

@unittest.skipUnless(bson.has_c(), "This test requires the C extension")
def test_encode_size_limit(self):
# PYTHON-5996: encoding must raise when a document's encoded size
# exceeds the BSON size limit.
big_value = "a" * (1 << 30)
with self.assertRaises(ValueError):
encode({"a": big_value, "b": big_value, "c": big_value})

def test_small_long_encode_decode(self):
encoded1 = encode({"x": 256})
decoded1 = decode(encoded1)["x"]
Expand Down
8 changes: 8 additions & 0 deletions test/test_encryption.py
Original file line number Diff line number Diff line change
Expand Up @@ -1293,6 +1293,14 @@ def test_04_kmip_endpoint_invalid_port(self):
with self.assertRaisesRegex(EncryptionError, "localhost:12345"):
self.client_encryption.create_data_key("kmip", master_key=master_key)

def test_kmip_endpoint_unix_socket_rejected(self):
# PYTHON-5990: a masterKey.endpoint ending in ".sock" must not be
# treated as a Unix domain socket path. KMS endpoints must be a TCP
# host[:port].
master_key = {"keyId": "1", "endpoint": "example.sock"}
with self.assertRaisesRegex(EncryptionError, "Invalid KMS endpoint"):
self.client_encryption.create_data_key("kmip", master_key=master_key)

@unittest.skipUnless(any(AWS_CREDS.values()), "AWS environment credentials are not set")
def test_05_aws_endpoint_wrong_region(self):
master_key = {
Expand Down
15 changes: 15 additions & 0 deletions test/test_uri_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,17 @@ def test_split_hosts(self):
self.assertEqual([("::1", 27017)], split_hosts("[::1]:27017"))
self.assertEqual([("::1", 27017)], split_hosts("[::1]"))

def test_split_hosts_percent_encoded_host(self):
# PYTHON-5986: percent-encoding in a hostname is rejected rather than
# decoded. Only socket paths and IPv6 zone indexes are decoded.
self.assertRaises(InvalidURI, split_hosts, "example.com%2Cexample.org%3A27017")
self.assertRaises(InvalidURI, split_hosts, "example.com%2F27017")

def test_split_hosts_ipv6_zone_index(self):
# An IPv6 zone index is escaped as "%25" (RFC 6874) and must still
# decode, unlike percent-encoding in a plain hostname.
self.assertEqual([("fe80::1%eth0", 27017)], split_hosts("[fe80::1%25eth0]:27017"))

def test_split_options(self):
self.assertRaises(ConfigurationError, split_options, "foo")
self.assertRaises(ConfigurationError, split_options, "foo=bar;foo")
Expand Down Expand Up @@ -672,6 +683,10 @@ def test_validate_uri_edge_cases(self):
self.assertRaises(InvalidURI, parse_uri, "mongodb://localhost/%24db")
self.assertRaises(InvalidURI, parse_uri, "mongodb://localhost/my%20db")

def test_validate_uri_percent_encoded_host(self):
# PYTHON-5986: a percent-encoded hostname is rejected by parse_uri too.
self.assertRaises(InvalidURI, parse_uri, "mongodb://example.com%2Cexample.org%3A27017/")

def test_validate_uri_srv_structure(self):
with patch("pymongo.uri_parser_shared._have_dnspython", return_value=True):
self.assertRaises(
Expand Down
Loading