Skip to content

Commit 0afa8ac

Browse files
ret2libcperlpunk
authored andcommitted
Prevents arbitrary code execution during python/object/new constructor (yaml#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 439424a commit 0afa8ac

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
@@ -33,6 +33,14 @@ def check_data(self):
3333
# If there are more documents available?
3434
return self.check_node()
3535

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

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

475493
def construct_python_str(self, node):
476494
return self.construct_scalar(node).encode('utf-8')
@@ -566,18 +584,23 @@ def make_python_instance(self, suffix, node,
566584
else:
567585
return cls(*args, **kwds)
568586

569-
def set_python_instance_state(self, instance, state):
587+
def set_python_instance_state(self, instance, state, unsafe=False):
570588
if hasattr(instance, '__setstate__'):
571589
instance.__setstate__(state)
572590
else:
573591
slotstate = {}
574592
if isinstance(state, tuple) and len(state) == 2:
575593
state, slotstate = state
576594
if hasattr(instance, '__dict__'):
595+
if not unsafe and state:
596+
for key in state.keys():
597+
self.check_state_key(key)
577598
instance.__dict__.update(state)
578599
elif state:
579600
slotstate.update(state)
580601
for key, value in slotstate.items():
602+
if not unsafe:
603+
self.check_state_key(key)
581604
setattr(object, key, value)
582605

583606
def construct_python_object(self, suffix, node):
@@ -699,6 +722,10 @@ def make_python_instance(self, suffix, node, args=None, kwds=None, newobj=False)
699722
return super(UnsafeConstructor, self).make_python_instance(
700723
suffix, node, args, kwds, newobj, unsafe=True)
701724

725+
def set_python_instance_state(self, instance, state):
726+
return super(UnsafeConstructor, self).set_python_instance_state(
727+
instance, state, unsafe=True)
728+
702729
UnsafeConstructor.add_multi_constructor(
703730
u'tag:yaml.org,2002:python/object/apply:',
704731
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():
@@ -471,6 +479,16 @@ def construct_undefined(self, node):
471479
SafeConstructor.construct_undefined)
472480

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

475493
def construct_python_str(self, node):
476494
return self.construct_scalar(node)
@@ -573,18 +591,23 @@ def make_python_instance(self, suffix, node,
573591
else:
574592
return cls(*args, **kwds)
575593

576-
def set_python_instance_state(self, instance, state):
594+
def set_python_instance_state(self, instance, state, unsafe=False):
577595
if hasattr(instance, '__setstate__'):
578596
instance.__setstate__(state)
579597
else:
580598
slotstate = {}
581599
if isinstance(state, tuple) and len(state) == 2:
582600
state, slotstate = state
583601
if hasattr(instance, '__dict__'):
602+
if not unsafe and state:
603+
for key in state.keys():
604+
self.check_state_key(key)
584605
instance.__dict__.update(state)
585606
elif state:
586607
slotstate.update(state)
587608
for key, value in slotstate.items():
609+
if not unsafe:
610+
self.check_state_key(key)
588611
setattr(object, key, value)
589612

590613
def construct_python_object(self, suffix, node):
@@ -710,6 +733,10 @@ def make_python_instance(self, suffix, node, args=None, kwds=None, newobj=False)
710733
return super(UnsafeConstructor, self).make_python_instance(
711734
suffix, node, args, kwds, newobj, unsafe=True)
712735

736+
def set_python_instance_state(self, instance, state):
737+
return super(UnsafeConstructor, self).set_python_instance_state(
738+
instance, state, unsafe=True)
739+
713740
UnsafeConstructor.add_multi_constructor(
714741
'tag:yaml.org,2002:python/object/apply:',
715742
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, MyInt, MyList, MyDict, \
20-
FixedOffset, today, execute
20+
FixedOffset, today, execute, MyFullLoader
2121

2222
class MyLoader(yaml.Loader):
2323
pass
@@ -213,6 +213,10 @@ def tzname(self, dt):
213213
def dst(self, dt):
214214
return datetime.timedelta(0)
215215

216+
class MyFullLoader(yaml.FullLoader):
217+
def get_state_keys_blacklist(self):
218+
return super(MyFullLoader, self).get_state_keys_blacklist() + ['^mymethod$', '^wrong_.*$']
219+
216220
today = datetime.date.today()
217221

218222
def _load_code(expression):
@@ -267,6 +271,18 @@ def test_constructor_types(data_filename, code_filename, verbose=False):
267271

268272
test_constructor_types.unittest = ['.data', '.code']
269273

274+
def test_subclass_blacklist_types(data_filename, verbose=False):
275+
_make_objects()
276+
try:
277+
yaml.load(open(data_filename, 'rb').read(), MyFullLoader)
278+
except yaml.YAMLError as exc:
279+
if verbose:
280+
print("%s:" % exc.__class__.__name__, exc)
281+
else:
282+
raise AssertionError("expected an exception")
283+
284+
test_subclass_blacklist_types.unittest = ['.subclass_blacklist']
285+
270286
if __name__ == '__main__':
271287
import sys, test_constructor
272288
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, MyInt, MyList, MyDict, \
17-
FixedOffset, today, execute
17+
FixedOffset, today, execute, MyFullLoader
1818

1919
class MyLoader(yaml.Loader):
2020
pass
@@ -200,6 +200,10 @@ def tzname(self, dt):
200200
def dst(self, dt):
201201
return datetime.timedelta(0)
202202

203+
class MyFullLoader(yaml.FullLoader):
204+
def get_state_keys_blacklist(self):
205+
return super().get_state_keys_blacklist() + ['^mymethod$', '^wrong_.*$']
206+
203207
today = datetime.date.today()
204208

205209
def _load_code(expression):
@@ -252,6 +256,18 @@ def test_constructor_types(data_filename, code_filename, verbose=False):
252256

253257
test_constructor_types.unittest = ['.data', '.code']
254258

259+
def test_subclass_blacklist_types(data_filename, verbose=False):
260+
_make_objects()
261+
try:
262+
yaml.load(open(data_filename, 'rb').read(), MyFullLoader)
263+
except yaml.YAMLError as exc:
264+
if verbose:
265+
print("%s:" % exc.__class__.__name__, exc)
266+
else:
267+
raise AssertionError("expected an exception")
268+
269+
test_subclass_blacklist_types.unittest = ['.subclass_blacklist']
270+
255271
if __name__ == '__main__':
256272
import sys, test_constructor
257273
sys.modules['test_constructor'] = sys.modules['__main__']

0 commit comments

Comments
 (0)