Skip to content

Commit

Permalink
Prevents arbitrary code execution during python/object/new constructor
Browse files Browse the repository at this point in the history
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 `state_blacklist_regexp` to include the
additional attributes they need, passing the subclassed loader to
yaml.load.
  • Loading branch information
ret2libc committed Mar 3, 2020
1 parent 2f463cf commit 8d9043c
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 2 deletions.
25 changes: 24 additions & 1 deletion lib/yaml/constructor.py
Expand Up @@ -56,6 +56,14 @@ def check_data(self):
# 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.state_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():
Expand Down Expand Up @@ -495,6 +503,12 @@ def construct_undefined(self, node):
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
STATE_BLACKLIST_KEYS = ['^extend$', '^__.*__$']

state_blacklist_regexp = re.compile('(' + '|'.join(STATE_BLACKLIST_KEYS) + ')')

def construct_python_str(self, node):
return self.construct_scalar(node).encode('utf-8')
Expand Down Expand Up @@ -590,18 +604,23 @@ def make_python_instance(self, suffix, node,
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:
slotstate = {}
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(instance, key, value)

def construct_python_object(self, suffix, node):
Expand Down Expand Up @@ -723,6 +742,10 @@ def make_python_instance(self, suffix, node, args=None, kwds=None, newobj=False)
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)
Expand Down
25 changes: 24 additions & 1 deletion lib3/yaml/constructor.py
Expand Up @@ -31,6 +31,14 @@ def check_data(self):
# 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.state_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():
Expand Down Expand Up @@ -472,6 +480,12 @@ def construct_undefined(self, node):
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
STATE_BLACKLIST_KEYS = ['^extend$', '^__.*__$']

state_blacklist_regexp = re.compile('(' + '|'.join(STATE_BLACKLIST_KEYS) + ')')

def construct_python_str(self, node):
return self.construct_scalar(node)
Expand Down Expand Up @@ -574,18 +588,23 @@ def make_python_instance(self, suffix, node,
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:
slotstate = {}
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(instance, key, value)

def construct_python_object(self, suffix, node):
Expand Down Expand Up @@ -711,6 +730,10 @@ def make_python_instance(self, suffix, node, args=None, kwds=None, newobj=False)
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)
Expand Down

0 comments on commit 8d9043c

Please sign in to comment.