summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorrobot-piglet <[email protected]>2025-09-19 19:22:05 +0300
committerrobot-piglet <[email protected]>2025-09-19 19:33:20 +0300
commit092f5ded19ef842075ccebbc2900afb5cf009ac5 (patch)
tree826e14c44cbe0ef758dc963ccd54842d07428688
parent1f2977c48482a39e97894637815ddb3579d0d44e (diff)
Intermediate changes
commit_hash:77545ccbe0cf9f22f5ee56187fc1fc43fe1bfe1c
-rw-r--r--contrib/python/pytest-mock/py3/.dist-info/METADATA5
-rw-r--r--contrib/python/pytest-mock/py3/pytest_mock/_util.py2
-rw-r--r--contrib/python/pytest-mock/py3/pytest_mock/_version.py19
-rw-r--r--contrib/python/pytest-mock/py3/pytest_mock/plugin.py34
-rw-r--r--contrib/python/pytest-mock/py3/tests/test_pytest_mock.py96
-rw-r--r--contrib/python/pytest-mock/py3/ya.make2
-rw-r--r--contrib/python/xmltodict/py3/.dist-info/METADATA39
-rw-r--r--contrib/python/xmltodict/py3/README.md24
-rw-r--r--contrib/python/xmltodict/py3/tests/test_dicttoxml.py32
-rw-r--r--contrib/python/xmltodict/py3/xmltodict.py22
-rw-r--r--contrib/python/xmltodict/py3/ya.make2
11 files changed, 225 insertions, 52 deletions
diff --git a/contrib/python/pytest-mock/py3/.dist-info/METADATA b/contrib/python/pytest-mock/py3/.dist-info/METADATA
index 701f4300081..5b6926488ea 100644
--- a/contrib/python/pytest-mock/py3/.dist-info/METADATA
+++ b/contrib/python/pytest-mock/py3/.dist-info/METADATA
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: pytest-mock
-Version: 3.14.1
+Version: 3.15.0
Summary: Thin-wrapper around the mock package for easier use with pytest
Author-email: Bruno Oliveira <[email protected]>
License: MIT
@@ -17,7 +17,6 @@ Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
-Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
@@ -25,7 +24,7 @@ Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Testing
-Requires-Python: >=3.8
+Requires-Python: >=3.9
Description-Content-Type: text/x-rst
License-File: LICENSE
Requires-Dist: pytest>=6.2.5
diff --git a/contrib/python/pytest-mock/py3/pytest_mock/_util.py b/contrib/python/pytest-mock/py3/pytest_mock/_util.py
index d3a732ac278..ad830caeefa 100644
--- a/contrib/python/pytest-mock/py3/pytest_mock/_util.py
+++ b/contrib/python/pytest-mock/py3/pytest_mock/_util.py
@@ -15,7 +15,7 @@ def get_mock_module(config):
config.getini("mock_use_standalone_module")
)
if use_standalone_module:
- import mock
+ from unittest import mock
_mock_module = mock
else:
diff --git a/contrib/python/pytest-mock/py3/pytest_mock/_version.py b/contrib/python/pytest-mock/py3/pytest_mock/_version.py
index 7c4035f98f9..7b8272a4140 100644
--- a/contrib/python/pytest-mock/py3/pytest_mock/_version.py
+++ b/contrib/python/pytest-mock/py3/pytest_mock/_version.py
@@ -1,7 +1,14 @@
# file generated by setuptools-scm
# don't change, don't track in version control
-__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
+__all__ = [
+ "__version__",
+ "__version_tuple__",
+ "version",
+ "version_tuple",
+ "__commit_id__",
+ "commit_id",
+]
TYPE_CHECKING = False
if TYPE_CHECKING:
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
from typing import Union
VERSION_TUPLE = Tuple[Union[int, str], ...]
+ COMMIT_ID = Union[str, None]
else:
VERSION_TUPLE = object
+ COMMIT_ID = object
version: str
__version__: str
__version_tuple__: VERSION_TUPLE
version_tuple: VERSION_TUPLE
+commit_id: COMMIT_ID
+__commit_id__: COMMIT_ID
-__version__ = version = '3.14.1'
-__version_tuple__ = version_tuple = (3, 14, 1)
+__version__ = version = '3.15.0'
+__version_tuple__ = version_tuple = (3, 15, 0)
+
+__commit_id__ = commit_id = None
diff --git a/contrib/python/pytest-mock/py3/pytest_mock/plugin.py b/contrib/python/pytest-mock/py3/pytest_mock/plugin.py
index 50dc06ab7cc..f4dbfc3ec6e 100644
--- a/contrib/python/pytest-mock/py3/pytest_mock/plugin.py
+++ b/contrib/python/pytest-mock/py3/pytest_mock/plugin.py
@@ -1,21 +1,18 @@
import builtins
import functools
import inspect
+import itertools
import unittest.mock
import warnings
+from collections.abc import Generator
+from collections.abc import Iterable
+from collections.abc import Iterator
+from collections.abc import Mapping
from dataclasses import dataclass
from dataclasses import field
from typing import Any
from typing import Callable
-from typing import Dict
-from typing import Generator
-from typing import Iterable
-from typing import Iterator
-from typing import List
-from typing import Mapping
from typing import Optional
-from typing import Tuple
-from typing import Type
from typing import TypeVar
from typing import Union
from typing import cast
@@ -52,7 +49,7 @@ class MockCache:
Cache MagicMock and Patcher instances so we can undo them later.
"""
- cache: List[MockCacheItem] = field(default_factory=list)
+ cache: list[MockCacheItem] = field(default_factory=list)
def _find(self, mock: MockType) -> MockCacheItem:
for mock_item in self.cache:
@@ -124,7 +121,7 @@ class MockerFixture:
:param bool return_value: Reset the return_value of mocks.
:param bool side_effect: Reset the side_effect of mocks.
"""
- supports_reset_mock_with_args: Tuple[Type[Any], ...]
+ supports_reset_mock_with_args: tuple[type[Any], ...]
if hasattr(self, "AsyncMock"):
supports_reset_mock_with_args = (self.Mock, self.AsyncMock)
else:
@@ -137,6 +134,8 @@ class MockerFixture:
# NOTE: The mock may be a dictionary
if hasattr(mock_item.mock, "spy_return_list"):
mock_item.mock.spy_return_list = []
+ if hasattr(mock_item.mock, "spy_return_iter"):
+ mock_item.mock.spy_return_iter = None
if isinstance(mock_item.mock, supports_reset_mock_with_args):
mock_item.mock.reset_mock(
return_value=return_value, side_effect=side_effect
@@ -178,6 +177,12 @@ class MockerFixture:
spy_obj.spy_exception = e
raise
else:
+ if isinstance(r, Iterator):
+ r, duplicated_iterator = itertools.tee(r, 2)
+ spy_obj.spy_return_iter = duplicated_iterator
+ else:
+ spy_obj.spy_return_iter = None
+
spy_obj.spy_return = r
spy_obj.spy_return_list.append(r)
return r
@@ -204,6 +209,7 @@ class MockerFixture:
spy_obj = self.patch.object(obj, name, side_effect=wrapped, autospec=autospec)
spy_obj.spy_return = None
+ spy_obj.spy_return_iter = None
spy_obj.spy_return_list = []
spy_obj.spy_exception = None
return spy_obj
@@ -338,7 +344,7 @@ class MockerFixture:
autospec: Optional[builtins.object] = None,
new_callable: Optional[builtins.object] = None,
**kwargs: Any,
- ) -> Dict[str, MockType]:
+ ) -> dict[str, MockType]:
"""API to mock.patch.multiple"""
return self._start_patch(
self.mock_module.patch.multiple,
@@ -355,7 +361,7 @@ class MockerFixture:
def dict(
self,
in_dict: Union[Mapping[Any, Any], str],
- values: Union[Mapping[Any, Any], Iterable[Tuple[Any, Any]]] = (),
+ values: Union[Mapping[Any, Any], Iterable[tuple[Any, Any]]] = (),
clear: bool = False,
**kwargs: Any,
) -> Any:
@@ -467,8 +473,8 @@ package_mocker = pytest.fixture(scope="package")(_mocker)
session_mocker = pytest.fixture(scope="session")(_mocker)
-_mock_module_patches = [] # type: List[Any]
-_mock_module_originals = {} # type: Dict[str, Any]
+_mock_module_patches: list[Any] = []
+_mock_module_originals: dict[str, Any] = {}
def assert_wrapper(
diff --git a/contrib/python/pytest-mock/py3/tests/test_pytest_mock.py b/contrib/python/pytest-mock/py3/tests/test_pytest_mock.py
index 43fdb3858b5..499c0a2b99d 100644
--- a/contrib/python/pytest-mock/py3/tests/test_pytest_mock.py
+++ b/contrib/python/pytest-mock/py3/tests/test_pytest_mock.py
@@ -3,12 +3,12 @@ import platform
import re
import sys
import warnings
+from collections.abc import Generator
+from collections.abc import Iterable
+from collections.abc import Iterator
from contextlib import contextmanager
from typing import Any
from typing import Callable
-from typing import Generator
-from typing import Tuple
-from typing import Type
from unittest.mock import AsyncMock
from unittest.mock import MagicMock
@@ -100,15 +100,15 @@ def check_unix_fs_mocked(
return check
-def mock_using_patch_object(mocker: MockerFixture) -> Tuple[MagicMock, MagicMock]:
+def mock_using_patch_object(mocker: MockerFixture) -> tuple[MagicMock, MagicMock]:
return mocker.patch.object(os, "remove"), mocker.patch.object(os, "listdir")
-def mock_using_patch(mocker: MockerFixture) -> Tuple[MagicMock, MagicMock]:
+def mock_using_patch(mocker: MockerFixture) -> tuple[MagicMock, MagicMock]:
return mocker.patch("os.remove"), mocker.patch("os.listdir")
-def mock_using_patch_multiple(mocker: MockerFixture) -> Tuple[MagicMock, MagicMock]:
+def mock_using_patch_multiple(mocker: MockerFixture) -> tuple[MagicMock, MagicMock]:
r = mocker.patch.multiple("os", remove=mocker.DEFAULT, listdir=mocker.DEFAULT)
return r["remove"], r["listdir"]
@@ -210,10 +210,7 @@ def test_mocker_resetall(mocker: MockerFixture) -> None:
assert isinstance(listdir.return_value, mocker.Mock)
assert open.side_effect is None
- if sys.version_info >= (3, 9):
- # The reset on child mocks have been implemented in 3.9
- # https://bugs.python.org/issue38932
- assert mocked_object.run.return_value != "mocked"
+ assert mocked_object.run.return_value != "mocked"
class TestMockerStub:
@@ -265,12 +262,14 @@ def test_instance_method_spy(mocker: MockerFixture) -> None:
assert other.bar(arg=10) == 20
foo.bar.assert_called_once_with(arg=10) # type:ignore[attr-defined]
assert foo.bar.spy_return == 20 # type:ignore[attr-defined]
+ assert foo.bar.spy_return_iter is None # type:ignore[attr-defined]
assert foo.bar.spy_return_list == [20] # type:ignore[attr-defined]
spy.assert_called_once_with(arg=10)
assert spy.spy_return == 20
assert foo.bar(arg=11) == 22
assert foo.bar(arg=12) == 24
assert spy.spy_return == 24
+ assert spy.spy_return_iter is None
assert spy.spy_return_list == [20, 22, 24]
@@ -287,7 +286,7 @@ def test_instance_method_spy(mocker: MockerFixture) -> None:
),
)
def test_instance_method_spy_exception(
- exc_cls: Type[BaseException],
+ exc_cls: type[BaseException],
mocker: MockerFixture,
) -> None:
class Foo:
@@ -349,11 +348,13 @@ def test_spy_reset(mocker: MockerFixture) -> None:
spy = mocker.spy(Foo, "bar")
assert spy.spy_return is None
+ assert spy.spy_return_iter is None
assert spy.spy_return_list == []
assert spy.spy_exception is None
Foo().bar(10)
assert spy.spy_return == 30
+ assert spy.spy_return_iter is None
assert spy.spy_return_list == [30]
assert spy.spy_exception is None
@@ -363,11 +364,13 @@ def test_spy_reset(mocker: MockerFixture) -> None:
with pytest.raises(ValueError):
Foo().bar(0)
assert spy.spy_return is None
+ assert spy.spy_return_iter is None
assert spy.spy_return_list == []
assert str(spy.spy_exception) == "invalid x"
Foo().bar(15)
assert spy.spy_return == 45
+ assert spy.spy_return_iter is None
assert spy.spy_return_list == [45]
assert spy.spy_exception is None
@@ -404,6 +407,7 @@ def test_instance_method_by_subclass_spy(mocker: MockerFixture) -> None:
calls = [mocker.call(foo, arg=10), mocker.call(other, arg=10)]
assert spy.call_args_list == calls
assert spy.spy_return == 20
+ assert spy.spy_return_iter is None
assert spy.spy_return_list == [20, 20]
@@ -418,9 +422,11 @@ def test_class_method_spy(mocker: MockerFixture) -> None:
assert Foo.bar(arg=10) == 20
Foo.bar.assert_called_once_with(arg=10) # type:ignore[attr-defined]
assert Foo.bar.spy_return == 20 # type:ignore[attr-defined]
+ assert Foo.bar.spy_return_iter is None # type:ignore[attr-defined]
assert Foo.bar.spy_return_list == [20] # type:ignore[attr-defined]
spy.assert_called_once_with(arg=10)
assert spy.spy_return == 20
+ assert spy.spy_return_iter is None
assert spy.spy_return_list == [20]
@@ -438,9 +444,11 @@ def test_class_method_subclass_spy(mocker: MockerFixture) -> None:
assert Foo.bar(arg=10) == 20
Foo.bar.assert_called_once_with(arg=10) # type:ignore[attr-defined]
assert Foo.bar.spy_return == 20 # type:ignore[attr-defined]
+ assert Foo.bar.spy_return_iter is None # type:ignore[attr-defined]
assert Foo.bar.spy_return_list == [20] # type:ignore[attr-defined]
spy.assert_called_once_with(arg=10)
assert spy.spy_return == 20
+ assert spy.spy_return_iter is None
assert spy.spy_return_list == [20]
@@ -460,9 +468,11 @@ def test_class_method_with_metaclass_spy(mocker: MockerFixture) -> None:
assert Foo.bar(arg=10) == 20
Foo.bar.assert_called_once_with(arg=10) # type:ignore[attr-defined]
assert Foo.bar.spy_return == 20 # type:ignore[attr-defined]
+ assert Foo.bar.spy_return_iter is None # type:ignore[attr-defined]
assert Foo.bar.spy_return_list == [20] # type:ignore[attr-defined]
spy.assert_called_once_with(arg=10)
assert spy.spy_return == 20
+ assert spy.spy_return_iter is None
assert spy.spy_return_list == [20]
@@ -477,9 +487,11 @@ def test_static_method_spy(mocker: MockerFixture) -> None:
assert Foo.bar(arg=10) == 20
Foo.bar.assert_called_once_with(arg=10) # type:ignore[attr-defined]
assert Foo.bar.spy_return == 20 # type:ignore[attr-defined]
+ assert Foo.bar.spy_return_iter is None # type:ignore[attr-defined]
assert Foo.bar.spy_return_list == [20] # type:ignore[attr-defined]
spy.assert_called_once_with(arg=10)
assert spy.spy_return == 20
+ assert spy.spy_return_iter is None
assert spy.spy_return_list == [20]
@@ -497,9 +509,11 @@ def test_static_method_subclass_spy(mocker: MockerFixture) -> None:
assert Foo.bar(arg=10) == 20
Foo.bar.assert_called_once_with(arg=10) # type:ignore[attr-defined]
assert Foo.bar.spy_return == 20 # type:ignore[attr-defined]
+ assert Foo.bar.spy_return_iter is None # type:ignore[attr-defined]
assert Foo.bar.spy_return_list == [20] # type:ignore[attr-defined]
spy.assert_called_once_with(arg=10)
assert spy.spy_return == 20
+ assert spy.spy_return_iter is None
assert spy.spy_return_list == [20]
@@ -522,9 +536,69 @@ def test_callable_like_spy(testdir: Any, mocker: MockerFixture) -> None:
uut.call_like(10)
spy.assert_called_once_with(10)
assert spy.spy_return == 20
+ assert spy.spy_return_iter is None
assert spy.spy_return_list == [20]
[email protected]("iterator", [(i for i in range(3)), iter([0, 1, 2])])
+def test_spy_return_iter(mocker: MockerFixture, iterator: Iterator[int]) -> None:
+ class Foo:
+ def bar(self) -> Iterator[int]:
+ return iterator
+
+ foo = Foo()
+ spy = mocker.spy(foo, "bar")
+ result = list(foo.bar())
+
+ assert result == [0, 1, 2]
+ assert spy.spy_return is not None
+ assert spy.spy_return_iter is not None
+ assert list(spy.spy_return_iter) == result
+
+ [return_value] = spy.spy_return_list
+ assert isinstance(return_value, Iterator)
+
+
[email protected]("iterable", [(0, 1, 2), [0, 1, 2], range(3)])
+def test_spy_return_iter_ignore_plain_iterable(
+ mocker: MockerFixture, iterable: Iterable[int]
+) -> None:
+ class Foo:
+ def bar(self) -> Iterable[int]:
+ return iterable
+
+ foo = Foo()
+ spy = mocker.spy(foo, "bar")
+ result = foo.bar()
+
+ assert result == iterable
+ assert spy.spy_return == result
+ assert spy.spy_return_iter is None
+ assert spy.spy_return_list == [result]
+
+
+def test_spy_return_iter_resets(mocker: MockerFixture) -> None:
+ class Foo:
+ iterables: Any = [
+ (i for i in range(3)),
+ 99,
+ ]
+
+ def bar(self) -> Any:
+ return self.iterables.pop(0)
+
+ foo = Foo()
+ spy = mocker.spy(foo, "bar")
+ result_iterator = list(foo.bar())
+
+ assert result_iterator == [0, 1, 2]
+ assert list(spy.spy_return_iter) == result_iterator
+
+ assert foo.bar() == 99
+ assert spy.spy_return_iter is None
+
+
async def test_instance_async_method_spy(mocker: MockerFixture) -> None:
class Foo:
async def bar(self, arg):
diff --git a/contrib/python/pytest-mock/py3/ya.make b/contrib/python/pytest-mock/py3/ya.make
index 3d080562b58..ec2d0bd0536 100644
--- a/contrib/python/pytest-mock/py3/ya.make
+++ b/contrib/python/pytest-mock/py3/ya.make
@@ -2,7 +2,7 @@
PY3_LIBRARY()
-VERSION(3.14.1)
+VERSION(3.15.0)
LICENSE(MIT)
diff --git a/contrib/python/xmltodict/py3/.dist-info/METADATA b/contrib/python/xmltodict/py3/.dist-info/METADATA
index 5a038d733d3..6462e0a6b5c 100644
--- a/contrib/python/xmltodict/py3/.dist-info/METADATA
+++ b/contrib/python/xmltodict/py3/.dist-info/METADATA
@@ -1,6 +1,6 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
Name: xmltodict
-Version: 0.14.2
+Version: 0.15.0
Summary: Makes working with XML feel like you are working with JSON
Home-page: https://github.com/martinblech/xmltodict
Author: Martin Blech
@@ -25,6 +25,17 @@ Classifier: Topic :: Text Processing :: Markup :: XML
Requires-Python: >=3.6
Description-Content-Type: text/markdown
License-File: LICENSE
+Dynamic: author
+Dynamic: author-email
+Dynamic: classifier
+Dynamic: description
+Dynamic: description-content-type
+Dynamic: home-page
+Dynamic: license
+Dynamic: license-file
+Dynamic: platform
+Dynamic: requires-python
+Dynamic: summary
# xmltodict
@@ -46,15 +57,15 @@ License-File: LICENSE
... """), indent=4))
{
"mydocument": {
- "@has": "an attribute",
+ "@has": "an attribute",
"and": {
"many": [
- "elements",
+ "elements",
"more elements"
]
- },
+ },
"plus": {
- "@a": "complex",
+ "@a": "complex",
"#text": "element as well"
}
}
@@ -110,7 +121,7 @@ True
>>> def handle_artist(_, artist):
... print(artist['name'])
... return True
->>>
+>>>
>>> xmltodict.parse(GzipFile('discogs_artists.xml.gz'),
... item_depth=2, item_callback=handle_artist)
A Perfect Circle
@@ -178,7 +189,7 @@ Text values for nodes can be specified with the `cdata_key` key in the python di
```python
>>> import xmltodict
->>>
+>>>
>>> mydict = {
... 'text': {
... '@color':'red',
@@ -234,7 +245,7 @@ $ pip install xmltodict
### Using conda
-For installing `xmltodict` using Anaconda/Miniconda (*conda*) from the
+For installing `xmltodict` using Anaconda/Miniconda (*conda*) from the
[conda-forge channel][#xmltodict-conda] all you need to do is:
[#xmltodict-conda]: https://anaconda.org/conda-forge/xmltodict
@@ -286,3 +297,13 @@ $ zypper in python2-xmltodict
# Python3
$ zypper in python3-xmltodict
```
+
+## Security Notes
+
+A CVE (CVE-2025-9375) was filed against `xmltodict` but is [disputed](https://github.com/martinblech/xmltodict/issues/377#issuecomment-3255691923). The root issue lies in Python’s `xml.sax.saxutils.XMLGenerator` API, which does not validate XML element names and provides no built-in way to do so. Since `xmltodict` is a thin wrapper that passes keys directly to `XMLGenerator`, the same issue exists in the standard library itself.
+
+It has been suggested that `xml.sax.saxutils.escape()` represents a secure usage path. This is incorrect: `escape()` is intended only for character data and attribute values, and can produce invalid XML when misapplied to element names. There is currently no secure, documented way in Python’s standard library to validate XML element names.
+
+Despite this, Fluid Attacks chose to assign a CVE to `xmltodict` while leaving the identical behavior in Python’s own standard library unaddressed. Their disclosure process also gave only 10 days from first contact to publication—well short of the 90-day industry norm—leaving no real opportunity for maintainer response. These actions reflect an inconsistency of standards and priorities that raise concerns about motivations, as they do not primarily serve the security of the broader community.
+
+The maintainer considers this CVE invalid and will formally dispute it with MITRE.
diff --git a/contrib/python/xmltodict/py3/README.md b/contrib/python/xmltodict/py3/README.md
index 6f776a8b4d8..4c24cf100f0 100644
--- a/contrib/python/xmltodict/py3/README.md
+++ b/contrib/python/xmltodict/py3/README.md
@@ -18,15 +18,15 @@
... """), indent=4))
{
"mydocument": {
- "@has": "an attribute",
+ "@has": "an attribute",
"and": {
"many": [
- "elements",
+ "elements",
"more elements"
]
- },
+ },
"plus": {
- "@a": "complex",
+ "@a": "complex",
"#text": "element as well"
}
}
@@ -82,7 +82,7 @@ True
>>> def handle_artist(_, artist):
... print(artist['name'])
... return True
->>>
+>>>
>>> xmltodict.parse(GzipFile('discogs_artists.xml.gz'),
... item_depth=2, item_callback=handle_artist)
A Perfect Circle
@@ -150,7 +150,7 @@ Text values for nodes can be specified with the `cdata_key` key in the python di
```python
>>> import xmltodict
->>>
+>>>
>>> mydict = {
... 'text': {
... '@color':'red',
@@ -206,7 +206,7 @@ $ pip install xmltodict
### Using conda
-For installing `xmltodict` using Anaconda/Miniconda (*conda*) from the
+For installing `xmltodict` using Anaconda/Miniconda (*conda*) from the
[conda-forge channel][#xmltodict-conda] all you need to do is:
[#xmltodict-conda]: https://anaconda.org/conda-forge/xmltodict
@@ -258,3 +258,13 @@ $ zypper in python2-xmltodict
# Python3
$ zypper in python3-xmltodict
```
+
+## Security Notes
+
+A CVE (CVE-2025-9375) was filed against `xmltodict` but is [disputed](https://github.com/martinblech/xmltodict/issues/377#issuecomment-3255691923). The root issue lies in Python’s `xml.sax.saxutils.XMLGenerator` API, which does not validate XML element names and provides no built-in way to do so. Since `xmltodict` is a thin wrapper that passes keys directly to `XMLGenerator`, the same issue exists in the standard library itself.
+
+It has been suggested that `xml.sax.saxutils.escape()` represents a secure usage path. This is incorrect: `escape()` is intended only for character data and attribute values, and can produce invalid XML when misapplied to element names. There is currently no secure, documented way in Python’s standard library to validate XML element names.
+
+Despite this, Fluid Attacks chose to assign a CVE to `xmltodict` while leaving the identical behavior in Python’s own standard library unaddressed. Their disclosure process also gave only 10 days from first contact to publication—well short of the 90-day industry norm—leaving no real opportunity for maintainer response. These actions reflect an inconsistency of standards and priorities that raise concerns about motivations, as they do not primarily serve the security of the broader community.
+
+The maintainer considers this CVE invalid and will formally dispute it with MITRE.
diff --git a/contrib/python/xmltodict/py3/tests/test_dicttoxml.py b/contrib/python/xmltodict/py3/tests/test_dicttoxml.py
index 470aca98a18..67e3a880979 100644
--- a/contrib/python/xmltodict/py3/tests/test_dicttoxml.py
+++ b/contrib/python/xmltodict/py3/tests/test_dicttoxml.py
@@ -231,3 +231,35 @@ xmlns:b="http://b.com/"><x a:attr="val">1</x><a:y>2</a:y><b:z>3</b:z></root>'''
expected_xml = '<?xml version="1.0" encoding="utf-8"?>\n<x>false</x>'
xml = unparse(dict(x=False))
self.assertEqual(xml, expected_xml)
+
+ def test_rejects_tag_name_with_angle_brackets(self):
+ # Minimal guard: disallow '<' or '>' to prevent breaking tag context
+ with self.assertRaises(ValueError):
+ unparse({"m><tag>content</tag": "unsafe"}, full_document=False)
+
+ def test_rejects_attribute_name_with_angle_brackets(self):
+ # Now we expect bad attribute names to be rejected
+ with self.assertRaises(ValueError):
+ unparse(
+ {"a": {"@m><tag>content</tag": "unsafe", "#text": "x"}},
+ full_document=False,
+ )
+
+ def test_rejects_malicious_xmlns_prefix(self):
+ # xmlns prefixes go under @xmlns mapping; reject angle brackets in prefix
+ with self.assertRaises(ValueError):
+ unparse(
+ {
+ "a": {
+ "@xmlns": {"m><bad": "http://example.com/"},
+ "#text": "x",
+ }
+ },
+ full_document=False,
+ )
+
+ def test_attribute_values_with_angle_brackets_are_escaped(self):
+ # Attribute values should be escaped by XMLGenerator
+ xml = unparse({"a": {"@attr": "1<middle>2", "#text": "x"}}, full_document=False)
+ # The generated XML should contain escaped '<' and '>' within the attribute value
+ self.assertIn('attr="1&lt;middle&gt;2"', xml)
diff --git a/contrib/python/xmltodict/py3/xmltodict.py b/contrib/python/xmltodict/py3/xmltodict.py
index 098f62762ae..c8491b354bd 100644
--- a/contrib/python/xmltodict/py3/xmltodict.py
+++ b/contrib/python/xmltodict/py3/xmltodict.py
@@ -14,7 +14,7 @@ if tuple(map(int, platform.python_version_tuple()[:2])) < (3, 7):
from inspect import isgenerator
__author__ = 'Martin Blech'
-__version__ = "0.14.2"
+__version__ = "0.15.0"
__license__ = 'MIT'
@@ -360,6 +360,14 @@ def parse(xml_input, encoding=None, expat=expat, process_namespaces=False,
return handler.item
+def _has_angle_brackets(value):
+ """Return True if value (a str) contains '<' or '>'.
+
+ Non-string values return False. Uses fast substring checks implemented in C.
+ """
+ return isinstance(value, str) and ("<" in value or ">" in value)
+
+
def _process_namespace(name, namespaces, ns_sep=':', attr_prefix='@'):
if not namespaces:
return name
@@ -393,6 +401,9 @@ def _emit(key, value, content_handler,
if result is None:
return
key, value = result
+ # Minimal validation to avoid breaking out of tag context
+ if _has_angle_brackets(key):
+ raise ValueError('Invalid element name: "<" or ">" not allowed')
if not hasattr(value, '__iter__') or isinstance(value, (str, dict)):
value = [value]
for index, v in enumerate(value):
@@ -421,12 +432,19 @@ def _emit(key, value, content_handler,
attr_prefix)
if ik == '@xmlns' and isinstance(iv, dict):
for k, v in iv.items():
+ if _has_angle_brackets(k):
+ raise ValueError(
+ 'Invalid attribute name: "<" or ">" not allowed'
+ )
attr = 'xmlns{}'.format(f':{k}' if k else '')
attrs[attr] = str(v)
continue
if not isinstance(iv, str):
iv = str(iv)
- attrs[ik[len(attr_prefix):]] = iv
+ attr_name = ik[len(attr_prefix) :]
+ if _has_angle_brackets(attr_name):
+ raise ValueError('Invalid attribute name: "<" or ">" not allowed')
+ attrs[attr_name] = iv
continue
children.append((ik, iv))
if isinstance(indent, int):
diff --git a/contrib/python/xmltodict/py3/ya.make b/contrib/python/xmltodict/py3/ya.make
index 7fdc1a14a9e..5dbf4dca8ff 100644
--- a/contrib/python/xmltodict/py3/ya.make
+++ b/contrib/python/xmltodict/py3/ya.make
@@ -2,7 +2,7 @@
PY3_LIBRARY()
-VERSION(0.14.2)
+VERSION(0.15.0)
LICENSE(MIT)