Skip to content

Commit 5080ba5

Browse files
authored
Prevents arbitrary code execution during python/object/new constructor (#386)
* 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
1 parent 2f463cf commit 5080ba5

File tree

6 files changed

+100
-4
lines changed

6 files changed

+100
-4
lines changed

lib/yaml/constructor.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ def check_data(self):
5656
# If there are more documents available?
5757
return self.check_node()
5858

59+
def check_state_key(self, key):
60+
"""Block special attributes/methods from being set in a newly created
61+
object, to prevent user-controlled methods from being called during
62+
deserialization"""
63+
if self.get_state_keys_blacklist_regexp().match(key):
64+
raise ConstructorError(None, None,
65+
"blacklisted key '%s' in instance state found" % (key,), None)
66+
5967
def get_data(self):
6068
# Construct and return the next document.
6169
if self.check_node():
@@ -495,6 +503,16 @@ def construct_undefined(self, node):
495503
SafeConstructor.construct_undefined)
496504

497505
class FullConstructor(SafeConstructor):
506+
# 'extend' is blacklisted because it is used by
507+
# construct_python_object_apply to add `listitems` to a newly generate
508+
# python instance
509+
def get_state_keys_blacklist(self):
510+
return ['^extend$', '^__.*__$']
511+
512+
def get_state_keys_blacklist_regexp(self):
513+
if not hasattr(self, 'state_keys_blacklist_regexp'):
514+
self.state_keys_blacklist_regexp = re.compile('(' + '|'.join(self.get_state_keys_blacklist()) + ')')
515+
return self.state_keys_blacklist_regexp
498516

499517
def construct_python_str(self, node):
500518
return self.construct_scalar(node).encode('utf-8')
@@ -590,18 +608,23 @@ def make_python_instance(self, suffix, node,
590608
else:
591609
return cls(*args, **kwds)
592610

593-
def set_python_instance_state(self, instance, state):
611+
def set_python_instance_state(self, instance, state, unsafe=False):
594612
if hasattr(instance, '__setstate__'):
595613
instance.__setstate__(state)
596614
else:
597615
slotstate = {}
598616
if isinstance(state, tuple) and len(state) == 2:
599617
state, slotstate = state
600618
if hasattr(instance, '__dict__'):
619+
if not unsafe and state:
620+
for key in state.keys():
621+
self.check_state_key(key)
601622
instance.__dict__.update(state)
602623
elif state:
603624
slotstate.update(state)
604625
for key, value in slotstate.items():
626+
if not unsafe:
627+
self.check_state_key(key)
605628
setattr(instance, key, value)
606629

607630
def construct_python_object(self, suffix, node):
@@ -723,6 +746,10 @@ def make_python_instance(self, suffix, node, args=None, kwds=None, newobj=False)
723746
return super(UnsafeConstructor, self).make_python_instance(
724747
suffix, node, args, kwds, newobj, unsafe=True)
725748

749+
def set_python_instance_state(self, instance, state):
750+
return super(UnsafeConstructor, self).set_python_instance_state(
751+
instance, state, unsafe=True)
752+
726753
UnsafeConstructor.add_multi_constructor(
727754
u'tag:yaml.org,2002:python/object/apply:',
728755
UnsafeConstructor.construct_python_object_apply)

lib3/yaml/constructor.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ def check_data(self):
3131
# If there are more documents available?
3232
return self.check_node()
3333

34+
def check_state_key(self, key):
35+
"""Block special attributes/methods from being set in a newly created
36+
object, to prevent user-controlled methods from being called during
37+
deserialization"""
38+
if self.get_state_keys_blacklist_regexp().match(key):
39+
raise ConstructorError(None, None,
40+
"blacklisted key '%s' in instance state found" % (key,), None)
41+
3442
def get_data(self):
3543
# Construct and return the next document.
3644
if self.check_node():
@@ -472,6 +480,16 @@ def construct_undefined(self, node):
472480
SafeConstructor.construct_undefined)
473481

474482
class FullConstructor(SafeConstructor):
483+
# 'extend' is blacklisted because it is used by
484+
# construct_python_object_apply to add `listitems` to a newly generate
485+
# python instance
486+
def get_state_keys_blacklist(self):
487+
return ['^extend$', '^__.*__$']
488+
489+
def get_state_keys_blacklist_regexp(self):
490+
if not hasattr(self, 'state_keys_blacklist_regexp'):
491+
self.state_keys_blacklist_regexp = re.compile('(' + '|'.join(self.get_state_keys_blacklist()) + ')')
492+
return self.state_keys_blacklist_regexp
475493

476494
def construct_python_str(self, node):
477495
return self.construct_scalar(node)
@@ -574,18 +592,23 @@ def make_python_instance(self, suffix, node,
574592
else:
575593
return cls(*args, **kwds)
576594

577-
def set_python_instance_state(self, instance, state):
595+
def set_python_instance_state(self, instance, state, unsafe=False):
578596
if hasattr(instance, '__setstate__'):
579597
instance.__setstate__(state)
580598
else:
581599
slotstate = {}
582600
if isinstance(state, tuple) and len(state) == 2:
583601
state, slotstate = state
584602
if hasattr(instance, '__dict__'):
603+
if not unsafe and state:
604+
for key in state.keys():
605+
self.check_state_key(key)
585606
instance.__dict__.update(state)
586607
elif state:
587608
slotstate.update(state)
588609
for key, value in slotstate.items():
610+
if not unsafe:
611+
self.check_state_key(key)
589612
setattr(instance, key, value)
590613

591614
def construct_python_object(self, suffix, node):
@@ -711,6 +734,10 @@ def make_python_instance(self, suffix, node, args=None, kwds=None, newobj=False)
711734
return super(UnsafeConstructor, self).make_python_instance(
712735
suffix, node, args, kwds, newobj, unsafe=True)
713736

737+
def set_python_instance_state(self, instance, state):
738+
return super(UnsafeConstructor, self).set_python_instance_state(
739+
instance, state, unsafe=True)
740+
714741
UnsafeConstructor.add_multi_constructor(
715742
'tag:yaml.org,2002:python/object/apply:',
716743
UnsafeConstructor.construct_python_object_apply)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
- !!python/object/new:yaml.MappingNode
2+
args:
3+
state:
4+
mymethod: test
5+
wrong_method: test2
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
- !!python/object/new:yaml.MappingNode
2+
args:
3+
state:
4+
extend: test
5+
__test__: test

tests/lib/test_constructor.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def _make_objects():
1717
global MyLoader, MyDumper, MyTestClass1, MyTestClass2, MyTestClass3, YAMLObject1, YAMLObject2, \
1818
AnObject, AnInstance, AState, ACustomState, InitArgs, InitArgsWithState, \
1919
NewArgs, NewArgsWithState, Reduce, ReduceWithState, Slots, MyInt, MyList, MyDict, \
20-
FixedOffset, today, execute
20+
FixedOffset, today, execute, MyFullLoader
2121

2222
class MyLoader(yaml.Loader):
2323
pass
@@ -235,6 +235,10 @@ def tzname(self, dt):
235235
def dst(self, dt):
236236
return datetime.timedelta(0)
237237

238+
class MyFullLoader(yaml.FullLoader):
239+
def get_state_keys_blacklist(self):
240+
return super(MyFullLoader, self).get_state_keys_blacklist() + ['^mymethod$', '^wrong_.*$']
241+
238242
today = datetime.date.today()
239243

240244
def _load_code(expression):
@@ -289,6 +293,18 @@ def test_constructor_types(data_filename, code_filename, verbose=False):
289293

290294
test_constructor_types.unittest = ['.data', '.code']
291295

296+
def test_subclass_blacklist_types(data_filename, verbose=False):
297+
_make_objects()
298+
try:
299+
yaml.load(open(data_filename, 'rb').read(), MyFullLoader)
300+
except yaml.YAMLError as exc:
301+
if verbose:
302+
print("%s:" % exc.__class__.__name__, exc)
303+
else:
304+
raise AssertionError("expected an exception")
305+
306+
test_subclass_blacklist_types.unittest = ['.subclass_blacklist']
307+
292308
if __name__ == '__main__':
293309
import sys, test_constructor
294310
sys.modules['test_constructor'] = sys.modules['__main__']

tests/lib3/test_constructor.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def _make_objects():
1414
global MyLoader, MyDumper, MyTestClass1, MyTestClass2, MyTestClass3, YAMLObject1, YAMLObject2, \
1515
AnObject, AnInstance, AState, ACustomState, InitArgs, InitArgsWithState, \
1616
NewArgs, NewArgsWithState, Reduce, ReduceWithState, Slots, MyInt, MyList, MyDict, \
17-
FixedOffset, today, execute
17+
FixedOffset, today, execute, MyFullLoader
1818

1919
class MyLoader(yaml.Loader):
2020
pass
@@ -222,6 +222,10 @@ def tzname(self, dt):
222222
def dst(self, dt):
223223
return datetime.timedelta(0)
224224

225+
class MyFullLoader(yaml.FullLoader):
226+
def get_state_keys_blacklist(self):
227+
return super().get_state_keys_blacklist() + ['^mymethod$', '^wrong_.*$']
228+
225229
today = datetime.date.today()
226230

227231
def _load_code(expression):
@@ -274,6 +278,18 @@ def test_constructor_types(data_filename, code_filename, verbose=False):
274278

275279
test_constructor_types.unittest = ['.data', '.code']
276280

281+
def test_subclass_blacklist_types(data_filename, verbose=False):
282+
_make_objects()
283+
try:
284+
yaml.load(open(data_filename, 'rb').read(), MyFullLoader)
285+
except yaml.YAMLError as exc:
286+
if verbose:
287+
print("%s:" % exc.__class__.__name__, exc)
288+
else:
289+
raise AssertionError("expected an exception")
290+
291+
test_subclass_blacklist_types.unittest = ['.subclass_blacklist']
292+
277293
if __name__ == '__main__':
278294
import sys, test_constructor
279295
sys.modules['test_constructor'] = sys.modules['__main__']

0 commit comments

Comments
 (0)