Skip to content

Commit 5759a1d

Browse files
committed
Serialization: add user-specified pickle whitelist
End users can now control which types can be unpickled using `Connection.pickle_whitelist_patterns`, which allows remote function call arguments of any type to be used.
1 parent 77fc745 commit 5759a1d

File tree

4 files changed

+71
-8
lines changed

4 files changed

+71
-8
lines changed

docs/howitworks.rst

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -505,16 +505,24 @@ The primary reason for using :py:mod:`cPickle` is that it is computationally
505505
efficient, and avoids including a potentially large body of serialization code
506506
in the bootstrap.
507507

508-
The pickler will instantiate only built-in types and one of 3 constructor
509-
functions, to support unpickling :py:class:`CallError
508+
The pickler will, by default, instantiate only built-in types and one of 3
509+
constructor functions, to support unpickling :py:class:`CallError
510510
<mitogen.core.CallError>`, :py:class:`mitogen.core.Sender`,and
511-
:py:class:`Context <mitogen.core.Context>`.
511+
:py:class:`Context <mitogen.core.Context>`. If you want to allow the deserialization
512+
of arbitrary types to, for example, allow passing remote function call arguments of an
513+
arbitrary type, you can use :py:func:`mitogen.core.set_pickle_whitelist` to set a
514+
list of allowable patterns that match against a global's
515+
:code:`[module].[func]` string.
512516

513517
The choice of Pickle is one area to be revisited later. All accounts suggest it
514518
cannot be used securely, however few of those accounts appear to be expert, and
515519
none mention any additional attacks that would not be prevented by using a
516520
restrictive class whitelist.
517521

522+
In the future, pickled data could include an HMAC that is based upon a
523+
preshared key (specified by the parent during child boot) to reduce the risk
524+
of malicioius tampering.
525+
518526

519527
The IO Multiplexer
520528
------------------

mitogen/core.py

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import os
4747
import pickle as py_pickle
4848
import pstats
49+
import re
4950
import signal
5051
import socket
5152
import struct
@@ -769,6 +770,45 @@ def find_class(self, module, func):
769770
_Unpickler = pickle.Unpickler
770771

771772

773+
#: A list of compiled regex patterns which allow end-users to selectively opt
774+
#: into deserializing certain globals.
775+
_PICKLE_GLOBAL_WHITELIST_PATTERNS = None
776+
_PICKLE_GLOBAL_WHITELIST = None
777+
778+
779+
def set_pickle_whitelist(pattern_strings):
780+
"""
781+
Specify regex patterns that control allowable global unpickling functions.
782+
783+
`pattern_strings` is sequence of pattern strings that will be fed into
784+
`re.compile` and then used to authenticate pickle calls. In order for a
785+
non-trivially typed message to unpickle, one of these patterns must
786+
match against a complete [module].[function] string.
787+
"""
788+
if not isinstance(pattern_strings, (tuple, list, set)):
789+
pattern_strings = (pattern_strings,)
790+
791+
global _PICKLE_GLOBAL_WHITELIST
792+
global _PICKLE_GLOBAL_WHITELIST_PATTERNS
793+
794+
_PICKLE_GLOBAL_WHITELIST = pattern_strings
795+
_PICKLE_GLOBAL_WHITELIST_PATTERNS = []
796+
797+
for patt_str in pattern_strings:
798+
if not patt_str.endswith('$'):
799+
patt_str += '$'
800+
_PICKLE_GLOBAL_WHITELIST_PATTERNS.append(re.compile(patt_str))
801+
802+
803+
def _test_pickle_whitelist_accept(module, func):
804+
if not _PICKLE_GLOBAL_WHITELIST_PATTERNS:
805+
return False
806+
807+
test_str = "{}.{}".format(module, func)
808+
return bool(any(
809+
patt.match(test_str) for patt in _PICKLE_GLOBAL_WHITELIST_PATTERNS))
810+
811+
772812
class Message(object):
773813
"""
774814
Messages are the fundamental unit of communication, comprising fields from
@@ -868,7 +908,14 @@ def _find_global(self, module, func):
868908
return BytesType
869909
elif SimpleNamespace and module == 'types' and func == 'SimpleNamespace':
870910
return SimpleNamespace
871-
raise StreamError('cannot unpickle %r/%r', module, func)
911+
elif _test_pickle_whitelist_accept(module, func):
912+
try:
913+
return getattr(import_module(module), func)
914+
except AttributeError as e:
915+
LOG.info(str(e))
916+
raise StreamError(
917+
'cannot unpickle %r/%r - try using `set_pickle_whitelist`',
918+
module, func)
872919

873920
@property
874921
def is_dead(self):
@@ -968,8 +1015,8 @@ def unpickle(self, throw=True, throw_dead=True):
9681015
# Must occur off the broker thread.
9691016
try:
9701017
obj = unpickler.load()
971-
except:
972-
LOG.error('raw pickle was: %r', self.data)
1018+
except Exception as e:
1019+
LOG.error('raw pickle was: %r (exc: %r)', self.data, e)
9731020
raise
9741021
self._unpickled = obj
9751022
except (TypeError, ValueError):
@@ -3845,6 +3892,9 @@ def _setup_master(self):
38453892
Router.max_message_size = self.config['max_message_size']
38463893
if self.config['profiling']:
38473894
enable_profiling()
3895+
if self.config['pickle_whitelist_patterns']:
3896+
set_pickle_whitelist(self.config['pickle_whitelist_patterns'])
3897+
38483898
self.broker = Broker(activate_compat=False)
38493899
self.router = Router(self.broker)
38503900
self.router.debug = self.config.get('debug', False)

mitogen/parent.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -639,7 +639,7 @@ def __init__(self):
639639
def get_timeout(self):
640640
"""
641641
Return the floating point seconds until the next event is due.
642-
642+
643643
:returns:
644644
Floating point delay, or 0.0, or :data:`None` if no events are
645645
scheduled.
@@ -1504,6 +1504,7 @@ def get_econtext_config(self):
15041504
'blacklist': self._router.get_module_blacklist(),
15051505
'max_message_size': self.options.max_message_size,
15061506
'version': mitogen.__version__,
1507+
'pickle_whitelist_patterns': mitogen.core._PICKLE_GLOBAL_WHITELIST,
15071508
}
15081509

15091510
def get_preamble(self):

tests/call_function_test.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,13 @@ def test_bad_return_value(self):
7777
lambda: self.local.call(func_with_bad_return_value))
7878
self.assertEqual(
7979
exc.args[0],
80-
"cannot unpickle '%s'/'CrazyType'" % (__name__,),
80+
"cannot unpickle '%s'/'CrazyType' - try using `set_pickle_whitelist`" % (__name__,),
8181
)
8282

83+
mitogen.core.set_pickle_whitelist(r'.*CrazyType')
84+
self.assertIsInstance(self.local.call(func_with_bad_return_value), CrazyType)
85+
mitogen.core.set_pickle_whitelist([])
86+
8387
def test_aborted_on_local_context_disconnect(self):
8488
stream = self.router._stream_by_id[self.local.context_id]
8589
self.broker.stop_receive(stream)

0 commit comments

Comments
 (0)