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
pull/1050/head
Alex Willmer 8 months ago
parent dfc3c7d516
commit d7979c3597

@ -21,6 +21,9 @@ Unreleased
---------- ----------
* :gh:issue:`974` Support Ansible 7 * :gh:issue:`974` Support Ansible 7
* :gh:issue:`1046` Raise :py:exc:`TypeError` in :func:`<mitogen.util.cast()>`
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) v0.3.5 (2024-03-17)

@ -190,10 +190,13 @@ PASSTHROUGH = (
def cast(obj): 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 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 functionality, such as annotating the safety of a Unicode string, or adding
additional methods to a dict. However, cPickle loves to preserve those additional methods to a dict. However :py:mod:`pickle` serializes these
subtypes during serialization, resulting in CallError during :meth:`call exactly, leading to :exc:`mitogen.CallError` during :meth:`Context.call
<mitogen.parent.Context.call>` in the target when it tries to deserialize <mitogen.parent.Context.call>` in the target when it tries to deserialize
the data. the data.
@ -201,6 +204,9 @@ def cast(obj):
custom sub-types removed. The functionality is not default since the custom sub-types removed. The functionality is not default since the
resulting walk may be computationally expensive given a large enough graph. 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. See :ref:`serialization-rules` for a list of supported types.
:param obj: :param obj:
@ -215,8 +221,16 @@ def cast(obj):
if isinstance(obj, PASSTHROUGH): if isinstance(obj, PASSTHROUGH):
return obj return obj
if isinstance(obj, mitogen.core.UnicodeType): if isinstance(obj, mitogen.core.UnicodeType):
return mitogen.core.UnicodeType(obj) return _cast(obj, mitogen.core.UnicodeType)
if isinstance(obj, mitogen.core.BytesType): 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)) 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

@ -44,6 +44,44 @@ class Unicode(mitogen.core.UnicodeType): pass
class Bytes(mitogen.core.BytesType): 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): class CastTest(testlib.TestCase):
def test_dict(self): def test_dict(self):
self.assertEqual(type(mitogen.utils.cast({})), dict) 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(b(''))), mitogen.core.BytesType)
self.assertEqual(type(mitogen.utils.cast(Bytes())), 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): def test_unknown(self):
self.assertRaises(TypeError, mitogen.utils.cast, set()) self.assertRaises(TypeError, mitogen.utils.cast, set())
self.assertRaises(TypeError, mitogen.utils.cast, 4j) self.assertRaises(TypeError, mitogen.utils.cast, 4j)

Loading…
Cancel
Save