From c8dad3f8e20191a1357f8ee234fa6f5f506052fc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tina=20M=C3=BCller?= <cpan2@tinita.de>
Date: Mon, 18 Nov 2019 15:55:27 +0100
Subject: [PATCH 1/2] Move constructor for object/apply to Unsafe

Original commit: 8c5e47fe62d7b9e0282a176a4b79b8b2980dc704
---
 lib/yaml/constructor.py  | 8 ++++----
 lib3/yaml/constructor.py | 8 ++++----
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/lib/yaml/constructor.py b/lib/yaml/constructor.py
index 516dad1..859c949 100644
--- a/lib/yaml/constructor.py
+++ b/lib/yaml/constructor.py
@@ -683,10 +683,6 @@ FullConstructor.add_multi_constructor(
     u'tag:yaml.org,2002:python/object:',
     FullConstructor.construct_python_object)
 
-FullConstructor.add_multi_constructor(
-    u'tag:yaml.org,2002:python/object/apply:',
-    FullConstructor.construct_python_object_apply)
-
 FullConstructor.add_multi_constructor(
     u'tag:yaml.org,2002:python/object/new:',
     FullConstructor.construct_python_object_new)
@@ -703,6 +699,10 @@ class UnsafeConstructor(FullConstructor):
         return super(UnsafeConstructor, self).make_python_instance(
             suffix, node, args, kwds, newobj, unsafe=True)
 
+UnsafeConstructor.add_multi_constructor(
+    u'tag:yaml.org,2002:python/object/apply:',
+    UnsafeConstructor.construct_python_object_apply)
+
 # Constructor is same as UnsafeConstructor. Need to leave this in place in case
 # people have extended it directly.
 class Constructor(UnsafeConstructor):
diff --git a/lib3/yaml/constructor.py b/lib3/yaml/constructor.py
index 34fc1ae..fb4f1e9 100644
--- a/lib3/yaml/constructor.py
+++ b/lib3/yaml/constructor.py
@@ -694,10 +694,6 @@ FullConstructor.add_multi_constructor(
     'tag:yaml.org,2002:python/object:',
     FullConstructor.construct_python_object)
 
-FullConstructor.add_multi_constructor(
-    'tag:yaml.org,2002:python/object/apply:',
-    FullConstructor.construct_python_object_apply)
-
 FullConstructor.add_multi_constructor(
     'tag:yaml.org,2002:python/object/new:',
     FullConstructor.construct_python_object_new)
@@ -714,6 +710,10 @@ class UnsafeConstructor(FullConstructor):
         return super(UnsafeConstructor, self).make_python_instance(
             suffix, node, args, kwds, newobj, unsafe=True)
 
+UnsafeConstructor.add_multi_constructor(
+    'tag:yaml.org,2002:python/object/apply:',
+    UnsafeConstructor.construct_python_object_apply)
+
 # Constructor is same as UnsafeConstructor. Need to leave this in place in case
 # people have extended it directly.
 class Constructor(UnsafeConstructor):
-- 
2.35.3


From c4b6fc94a5909f2d63ee815b36ed3bc8db42043f Mon Sep 17 00:00:00 2001
From: Riccardo Schirone <ret2libc@users.noreply.github.com>
Date: Tue, 17 Mar 2020 19:09:55 +0100
Subject: [PATCH 2/2] Prevents arbitrary code execution during
 python/object/new constructor (#386)

Original commit: 5080ba513377b6355a0502104846ee804656f1e0

* Prevents arbitrary code execution during python/object/new constructor

In FullLoader python/object/new constructor, implemented by
construct_python_object_apply, has support for setting the state of a
deserialized instance through the set_python_instance_state method.
After setting the state, some operations are performed on the instance
to complete its initialization, however it is possible for an attacker
to set the instance' state in such a way that arbitrary code is executed
by the FullLoader.

This patch tries to block such attacks in FullLoader by preventing
set_python_instance_state from setting arbitrary properties. It
implements a blacklist that includes `extend` method (called by
construct_python_object_apply) and all special methods (e.g. __set__,
__setitem__, etc.).

Users who need special attributes being set in the state of a
deserialized object can still do it through the UnsafeLoader, which
however should not be used on untrusted input. Additionally, they can
subclass FullLoader and redefine `get_state_keys_blacklist()` to
extend/replace the list of blacklisted keys, passing the subclassed
loader to yaml.load.

* Make sure python/object/new constructor does not set some properties

* Add test to show how to subclass FullLoader with new blacklist
---
 lib/yaml/constructor.py                       | 29 ++++++++++++++++++-
 lib3/yaml/constructor.py                      | 29 ++++++++++++++++++-
 tests/data/myfullloader.subclass_blacklist    |  5 ++++
 ...erwrite-state-new-constructor.loader-error |  5 ++++
 tests/lib/test_constructor.py                 | 18 +++++++++++-
 tests/lib3/test_constructor.py                | 18 +++++++++++-
 6 files changed, 100 insertions(+), 4 deletions(-)
 create mode 100644 tests/data/myfullloader.subclass_blacklist
 create mode 100644 tests/data/overwrite-state-new-constructor.loader-error

diff --git a/lib/yaml/constructor.py b/lib/yaml/constructor.py
index 859c949..a54758d 100644
--- a/lib/yaml/constructor.py
+++ b/lib/yaml/constructor.py
@@ -33,6 +33,14 @@ class BaseConstructor(object):
         # If there are more documents available?
         return self.check_node()
 
+    def check_state_key(self, key):
+        """Block special attributes/methods from being set in a newly created
+        object, to prevent user-controlled methods from being called during
+        deserialization"""
+        if self.get_state_keys_blacklist_regexp().match(key):
+            raise ConstructorError(None, None,
+                "blacklisted key '%s' in instance state found" % (key,), None)
+
     def get_data(self):
         # Construct and return the next document.
         if self.check_node():
@@ -471,6 +479,16 @@ SafeConstructor.add_constructor(None,
         SafeConstructor.construct_undefined)
 
 class FullConstructor(SafeConstructor):
+    # 'extend' is blacklisted because it is used by
+    # construct_python_object_apply to add `listitems` to a newly generate
+    # python instance
+    def get_state_keys_blacklist(self):
+        return ['^extend$', '^__.*__$']
+
+    def get_state_keys_blacklist_regexp(self):
+        if not hasattr(self, 'state_keys_blacklist_regexp'):
+            self.state_keys_blacklist_regexp = re.compile('(' + '|'.join(self.get_state_keys_blacklist()) + ')')
+        return self.state_keys_blacklist_regexp
 
     def construct_python_str(self, node):
         return self.construct_scalar(node).encode('utf-8')
@@ -566,7 +584,7 @@ class FullConstructor(SafeConstructor):
         else:
             return cls(*args, **kwds)
 
-    def set_python_instance_state(self, instance, state):
+    def set_python_instance_state(self, instance, state, unsafe=False):
         if hasattr(instance, '__setstate__'):
             instance.__setstate__(state)
         else:
@@ -574,10 +592,15 @@ class FullConstructor(SafeConstructor):
             if isinstance(state, tuple) and len(state) == 2:
                 state, slotstate = state
             if hasattr(instance, '__dict__'):
+                if not unsafe and state:
+                    for key in state.keys():
+                        self.check_state_key(key)
                 instance.__dict__.update(state)
             elif state:
                 slotstate.update(state)
             for key, value in slotstate.items():
+                if not unsafe:
+                    self.check_state_key(key)
                 setattr(object, key, value)
 
     def construct_python_object(self, suffix, node):
@@ -699,6 +722,10 @@ class UnsafeConstructor(FullConstructor):
         return super(UnsafeConstructor, self).make_python_instance(
             suffix, node, args, kwds, newobj, unsafe=True)
 
+    def set_python_instance_state(self, instance, state):
+        return super(UnsafeConstructor, self).set_python_instance_state(
+            instance, state, unsafe=True)
+
 UnsafeConstructor.add_multi_constructor(
     u'tag:yaml.org,2002:python/object/apply:',
     UnsafeConstructor.construct_python_object_apply)
diff --git a/lib3/yaml/constructor.py b/lib3/yaml/constructor.py
index fb4f1e9..40974af 100644
--- a/lib3/yaml/constructor.py
+++ b/lib3/yaml/constructor.py
@@ -31,6 +31,14 @@ class BaseConstructor:
         # If there are more documents available?
         return self.check_node()
 
+    def check_state_key(self, key):
+        """Block special attributes/methods from being set in a newly created
+        object, to prevent user-controlled methods from being called during
+        deserialization"""
+        if self.get_state_keys_blacklist_regexp().match(key):
+            raise ConstructorError(None, None,
+                "blacklisted key '%s' in instance state found" % (key,), None)
+
     def get_data(self):
         # Construct and return the next document.
         if self.check_node():
@@ -471,6 +479,16 @@ SafeConstructor.add_constructor(None,
         SafeConstructor.construct_undefined)
 
 class FullConstructor(SafeConstructor):
+    # 'extend' is blacklisted because it is used by
+    # construct_python_object_apply to add `listitems` to a newly generate
+    # python instance
+    def get_state_keys_blacklist(self):
+        return ['^extend$', '^__.*__$']
+
+    def get_state_keys_blacklist_regexp(self):
+        if not hasattr(self, 'state_keys_blacklist_regexp'):
+            self.state_keys_blacklist_regexp = re.compile('(' + '|'.join(self.get_state_keys_blacklist()) + ')')
+        return self.state_keys_blacklist_regexp
 
     def construct_python_str(self, node):
         return self.construct_scalar(node)
@@ -573,7 +591,7 @@ class FullConstructor(SafeConstructor):
         else:
             return cls(*args, **kwds)
 
-    def set_python_instance_state(self, instance, state):
+    def set_python_instance_state(self, instance, state, unsafe=False):
         if hasattr(instance, '__setstate__'):
             instance.__setstate__(state)
         else:
@@ -581,10 +599,15 @@ class FullConstructor(SafeConstructor):
             if isinstance(state, tuple) and len(state) == 2:
                 state, slotstate = state
             if hasattr(instance, '__dict__'):
+                if not unsafe and state:
+                    for key in state.keys():
+                        self.check_state_key(key)
                 instance.__dict__.update(state)
             elif state:
                 slotstate.update(state)
             for key, value in slotstate.items():
+                if not unsafe:
+                    self.check_state_key(key)
                 setattr(object, key, value)
 
     def construct_python_object(self, suffix, node):
@@ -710,6 +733,10 @@ class UnsafeConstructor(FullConstructor):
         return super(UnsafeConstructor, self).make_python_instance(
             suffix, node, args, kwds, newobj, unsafe=True)
 
+    def set_python_instance_state(self, instance, state):
+        return super(UnsafeConstructor, self).set_python_instance_state(
+            instance, state, unsafe=True)
+
 UnsafeConstructor.add_multi_constructor(
     'tag:yaml.org,2002:python/object/apply:',
     UnsafeConstructor.construct_python_object_apply)
diff --git a/tests/data/myfullloader.subclass_blacklist b/tests/data/myfullloader.subclass_blacklist
new file mode 100644
index 0000000..555a2b3
--- /dev/null
+++ b/tests/data/myfullloader.subclass_blacklist
@@ -0,0 +1,5 @@
+- !!python/object/new:yaml.MappingNode
+  args:
+  state:
+    mymethod: test
+    wrong_method: test2
diff --git a/tests/data/overwrite-state-new-constructor.loader-error b/tests/data/overwrite-state-new-constructor.loader-error
new file mode 100644
index 0000000..8d224f1
--- /dev/null
+++ b/tests/data/overwrite-state-new-constructor.loader-error
@@ -0,0 +1,5 @@
+- !!python/object/new:yaml.MappingNode
+  args:
+  state:
+    extend: test
+    __test__: test
diff --git a/tests/lib/test_constructor.py b/tests/lib/test_constructor.py
index beee7b0..a18d13d 100644
--- a/tests/lib/test_constructor.py
+++ b/tests/lib/test_constructor.py
@@ -17,7 +17,7 @@ def _make_objects():
     global MyLoader, MyDumper, MyTestClass1, MyTestClass2, MyTestClass3, YAMLObject1, YAMLObject2,  \
             AnObject, AnInstance, AState, ACustomState, InitArgs, InitArgsWithState,    \
             NewArgs, NewArgsWithState, Reduce, ReduceWithState, MyInt, MyList, MyDict,  \
-            FixedOffset, today, execute
+            FixedOffset, today, execute, MyFullLoader
 
     class MyLoader(yaml.Loader):
         pass
@@ -213,6 +213,10 @@ def _make_objects():
         def dst(self, dt):
             return datetime.timedelta(0)
 
+    class MyFullLoader(yaml.FullLoader):
+        def get_state_keys_blacklist(self):
+            return super(MyFullLoader, self).get_state_keys_blacklist() + ['^mymethod$', '^wrong_.*$']
+
     today = datetime.date.today()
 
 def _load_code(expression):
@@ -267,6 +271,18 @@ def test_constructor_types(data_filename, code_filename, verbose=False):
 
 test_constructor_types.unittest = ['.data', '.code']
 
+def test_subclass_blacklist_types(data_filename, verbose=False):
+    _make_objects()
+    try:
+        yaml.load(open(data_filename, 'rb').read(), MyFullLoader)
+    except yaml.YAMLError as exc:
+        if verbose:
+            print("%s:" % exc.__class__.__name__, exc)
+    else:
+        raise AssertionError("expected an exception")
+
+test_subclass_blacklist_types.unittest = ['.subclass_blacklist']
+
 if __name__ == '__main__':
     import sys, test_constructor
     sys.modules['test_constructor'] = sys.modules['__main__']
diff --git a/tests/lib3/test_constructor.py b/tests/lib3/test_constructor.py
index 427f53c..fb4509e 100644
--- a/tests/lib3/test_constructor.py
+++ b/tests/lib3/test_constructor.py
@@ -14,7 +14,7 @@ def _make_objects():
     global MyLoader, MyDumper, MyTestClass1, MyTestClass2, MyTestClass3, YAMLObject1, YAMLObject2,  \
             AnObject, AnInstance, AState, ACustomState, InitArgs, InitArgsWithState,    \
             NewArgs, NewArgsWithState, Reduce, ReduceWithState, MyInt, MyList, MyDict,  \
-            FixedOffset, today, execute
+            FixedOffset, today, execute, MyFullLoader
 
     class MyLoader(yaml.Loader):
         pass
@@ -200,6 +200,10 @@ def _make_objects():
         def dst(self, dt):
             return datetime.timedelta(0)
 
+    class MyFullLoader(yaml.FullLoader):
+        def get_state_keys_blacklist(self):
+            return super().get_state_keys_blacklist() + ['^mymethod$', '^wrong_.*$']
+
     today = datetime.date.today()
 
 def _load_code(expression):
@@ -252,6 +256,18 @@ def test_constructor_types(data_filename, code_filename, verbose=False):
 
 test_constructor_types.unittest = ['.data', '.code']
 
+def test_subclass_blacklist_types(data_filename, verbose=False):
+    _make_objects()
+    try:
+        yaml.load(open(data_filename, 'rb').read(), MyFullLoader)
+    except yaml.YAMLError as exc:
+        if verbose:
+            print("%s:" % exc.__class__.__name__, exc)
+    else:
+        raise AssertionError("expected an exception")
+
+test_subclass_blacklist_types.unittest = ['.subclass_blacklist']
+
 if __name__ == '__main__':
     import sys, test_constructor
     sys.modules['test_constructor'] = sys.modules['__main__']
-- 
2.35.3

