From d7979c35978e1d4300ec5b883e37959dd315e1ce Mon Sep 17 00:00:00 2001 From: Alex Willmer Date: Thu, 28 Mar 2024 11:25:13 +0000 Subject: [PATCH] mitogen: Raise TypeError on `mitogen.utils.cast(custom_str)` failures If casting a string fails then raise a TypeError. This is potentially an API breaking change; chosen as the lesser evil vs. allowing silent errors. `cast()` relies on `bytes(obj)` & `str(obj)` returning the respective supertype. That's no longer the case for `AnsibleUnsafeBytes` & `AnsibleUnsafeText`; since fixes/mitigations for CVE-2023-5764. fixes #1046, refs #977 See also - https://github.com/advisories/GHSA-7j69-qfc3-2fq9 - https://github.com/ansible/ansible/pull/82293 --- docs/changelog.rst | 3 +++ mitogen/utils.py | 22 +++++++++++++++++---- tests/utils_test.py | 47 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f5fd7a4b..ac20653b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -21,6 +21,9 @@ Unreleased ---------- * :gh:issue:`974` Support Ansible 7 +* :gh:issue:`1046` Raise :py:exc:`TypeError` in :func:`` + when casting a string subtype to `bytes()` or `str()` fails. This is + potentially an API breaking change. Failures previously passed silently. v0.3.5 (2024-03-17) diff --git a/mitogen/utils.py b/mitogen/utils.py index 71f7c35f..1fbf71fe 100644 --- a/mitogen/utils.py +++ b/mitogen/utils.py @@ -190,10 +190,13 @@ PASSTHROUGH = ( def cast(obj): """ + Return obj (or a copy) with subtypes of builtins cast to their supertype. + Subtypes of those in :data:`PASSTHROUGH` are not modified. + Many tools love to subclass built-in types in order to implement useful functionality, such as annotating the safety of a Unicode string, or adding - additional methods to a dict. However, cPickle loves to preserve those - subtypes during serialization, resulting in CallError during :meth:`call + additional methods to a dict. However :py:mod:`pickle` serializes these + exactly, leading to :exc:`mitogen.CallError` during :meth:`Context.call ` in the target when it tries to deserialize the data. @@ -201,6 +204,9 @@ def cast(obj): custom sub-types removed. The functionality is not default since the resulting walk may be computationally expensive given a large enough graph. + Raises :py:exc:`TypeError` if an unknown subtype is encountered, or + casting does not return the desired supertype. + See :ref:`serialization-rules` for a list of supported types. :param obj: @@ -215,8 +221,16 @@ def cast(obj): if isinstance(obj, PASSTHROUGH): return obj if isinstance(obj, mitogen.core.UnicodeType): - return mitogen.core.UnicodeType(obj) + return _cast(obj, mitogen.core.UnicodeType) if isinstance(obj, mitogen.core.BytesType): - return mitogen.core.BytesType(obj) + return _cast(obj, mitogen.core.BytesType) raise TypeError("Cannot serialize: %r: %r" % (type(obj), obj)) + + +def _cast(obj, desired_type): + result = desired_type(obj) + if type(result) is not desired_type: + raise TypeError("Cast of %r to %r failed, got %r" + % (type(obj), desired_type, type(result))) + return result diff --git a/tests/utils_test.py b/tests/utils_test.py index 6c71d33c..37246961 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -44,6 +44,44 @@ class Unicode(mitogen.core.UnicodeType): pass class Bytes(mitogen.core.BytesType): pass +class StubbornBytes(mitogen.core.BytesType): + """ + A binary string type that persists through `bytes(...)`. + + Stand-in for `AnsibleUnsafeBytes()` in Ansible 7-9 (core 2.14-2.16), after + fixes/mitigations for CVE-2023-5764. + """ + if mitogen.core.PY3: + def __bytes__(self): return self + def __str__(self): return self.decode() + else: + def __str__(self): return self + def __unicode__(self): return self.decode() + + def decode(self, encoding='utf-8', errors='strict'): + s = super(StubbornBytes).encode(encoding=encoding, errors=errors) + return StubbornText(s) + + +class StubbornText(mitogen.core.UnicodeType): + """ + A text string type that persists through `unicode(...)` or `str(...)`. + + Stand-in for `AnsibleUnsafeText()` in Ansible 7-9 (core 2.14-2.16), after + following fixes/mitigations for CVE-2023-5764. + """ + if mitogen.core.PY3: + def __bytes__(self): return self.encode() + def __str__(self): return self + else: + def __str__(self): return self.encode() + def __unicode__(self): return self + + def encode(self, encoding='utf-8', errors='strict'): + s = super(StubbornText).encode(encoding=encoding, errors=errors) + return StubbornBytes(s) + + class CastTest(testlib.TestCase): def test_dict(self): self.assertEqual(type(mitogen.utils.cast({})), dict) @@ -91,6 +129,15 @@ class CastTest(testlib.TestCase): self.assertEqual(type(mitogen.utils.cast(b(''))), mitogen.core.BytesType) self.assertEqual(type(mitogen.utils.cast(Bytes())), mitogen.core.BytesType) + def test_stubborn_types_raise(self): + stubborn_bytes = StubbornBytes(b('abc')) + self.assertIs(stubborn_bytes, mitogen.core.BytesType(stubborn_bytes)) + self.assertRaises(TypeError, mitogen.utils.cast, stubborn_bytes) + + stubborn_text = StubbornText(u'abc') + self.assertIs(stubborn_text, mitogen.core.UnicodeType(stubborn_text)) + self.assertRaises(TypeError, mitogen.utils.cast, stubborn_text) + def test_unknown(self): self.assertRaises(TypeError, mitogen.utils.cast, set()) self.assertRaises(TypeError, mitogen.utils.cast, 4j)