diff options
author | robot-piglet <[email protected]> | 2025-09-19 19:22:05 +0300 |
---|---|---|
committer | robot-piglet <[email protected]> | 2025-09-19 19:33:20 +0300 |
commit | 092f5ded19ef842075ccebbc2900afb5cf009ac5 (patch) | |
tree | 826e14c44cbe0ef758dc963ccd54842d07428688 | |
parent | 1f2977c48482a39e97894637815ddb3579d0d44e (diff) |
Intermediate changes
commit_hash:77545ccbe0cf9f22f5ee56187fc1fc43fe1bfe1c
-rw-r--r-- | contrib/python/pytest-mock/py3/.dist-info/METADATA | 5 | ||||
-rw-r--r-- | contrib/python/pytest-mock/py3/pytest_mock/_util.py | 2 | ||||
-rw-r--r-- | contrib/python/pytest-mock/py3/pytest_mock/_version.py | 19 | ||||
-rw-r--r-- | contrib/python/pytest-mock/py3/pytest_mock/plugin.py | 34 | ||||
-rw-r--r-- | contrib/python/pytest-mock/py3/tests/test_pytest_mock.py | 96 | ||||
-rw-r--r-- | contrib/python/pytest-mock/py3/ya.make | 2 | ||||
-rw-r--r-- | contrib/python/xmltodict/py3/.dist-info/METADATA | 39 | ||||
-rw-r--r-- | contrib/python/xmltodict/py3/README.md | 24 | ||||
-rw-r--r-- | contrib/python/xmltodict/py3/tests/test_dicttoxml.py | 32 | ||||
-rw-r--r-- | contrib/python/xmltodict/py3/xmltodict.py | 22 | ||||
-rw-r--r-- | contrib/python/xmltodict/py3/ya.make | 2 |
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<middle>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) |