Skip to content

Commit 33320af

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 8b88cb4 commit 33320af

File tree

4 files changed

+58
-7
lines changed

4 files changed

+58
-7
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 specify :py:attr:`Connection.pickle_whitelist_patterns` as 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: 36 additions & 1 deletion
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
@@ -755,6 +756,30 @@ def find_class(self, module, func):
755756
_Unpickler = pickle.Unpickler
756757

757758

759+
#: A compiled regexp which allows end-users to selectively opt into deserializing
760+
#: certain globals.
761+
_PICKLE_GLOBAL_WHITELIST_PATTERNS = None
762+
763+
764+
def _set_pickle_whitelist(pattern_strings):
765+
global _PICKLE_GLOBAL_WHITELIST_PATTERNS
766+
_PICKLE_GLOBAL_WHITELIST_PATTERNS = []
767+
768+
for patt_str in pattern_strings:
769+
if not patt_str.endswith('$'):
770+
patt_str += '$'
771+
_PICKLE_GLOBAL_WHITELIST_PATTERNS.append(re.compile(patt_str))
772+
773+
774+
def _test_pickle_whitelist_accept(module, func):
775+
if not _PICKLE_GLOBAL_WHITELIST_PATTERNS:
776+
return False
777+
778+
test_str = "{}.{}".format(module, func)
779+
return bool(any(
780+
patt.match(test_str) for patt in _PICKLE_GLOBAL_WHITELIST_PATTERNS))
781+
782+
758783
class Message(object):
759784
"""
760785
Messages are the fundamental unit of communication, comprising fields from
@@ -854,7 +879,14 @@ def _find_global(self, module, func):
854879
return BytesType
855880
elif SimpleNamespace and module == 'types' and func == 'SimpleNamespace':
856881
return SimpleNamespace
857-
raise StreamError('cannot unpickle %r/%r', module, func)
882+
elif _test_pickle_whitelist_accept(module, func):
883+
try:
884+
return getattr(import_module(module), func)
885+
except AttributeError as e:
886+
LOG.info(str(e))
887+
raise StreamError(
888+
'cannot unpickle %r/%r - try using `pickle_whitelist_patterns`',
889+
module, func)
858890

859891
@property
860892
def is_dead(self):
@@ -3816,6 +3848,9 @@ def _setup_master(self):
38163848
Router.max_message_size = self.config['max_message_size']
38173849
if self.config['profiling']:
38183850
enable_profiling()
3851+
if self.config['pickle_whitelist_patterns']:
3852+
_set_pickle_whitelist(self.config['pickle_whitelist_patterns'])
3853+
38193854
self.broker = Broker(activate_compat=False)
38203855
self.router = Router(self.broker)
38213856
self.router.debug = self.config.get('debug', False)

mitogen/parent.py

Lines changed: 10 additions & 2 deletions
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.
@@ -1306,9 +1306,15 @@ class Options(object):
13061306
#: UNIX timestamp after which the connection attempt should be abandoned.
13071307
connect_deadline = None
13081308

1309+
#: A sequence of pattern strings that will be fed into `re.compile` and then used
1310+
#: to authenticate pickle calls. One of these patterns must match against a
1311+
#: complete [module].[function] string in order to allow unpickling .
1312+
pickle_whitelist_patterns = None
1313+
13091314
def __init__(self, max_message_size, name=None, remote_name=None,
13101315
python_path=None, debug=False, connect_timeout=None,
1311-
profiling=False, unidirectional=False, old_router=None):
1316+
profiling=False, unidirectional=False, old_router=None,
1317+
pickle_whitelist_patterns=None):
13121318
self.name = name
13131319
self.max_message_size = max_message_size
13141320
if python_path:
@@ -1326,6 +1332,7 @@ def __init__(self, max_message_size, name=None, remote_name=None,
13261332
self.unidirectional = unidirectional
13271333
self.max_message_size = max_message_size
13281334
self.connect_deadline = mitogen.core.now() + self.connect_timeout
1335+
self.pickle_whitelist_patterns = pickle_whitelist_patterns
13291336

13301337

13311338
class Connection(object):
@@ -1504,6 +1511,7 @@ def get_econtext_config(self):
15041511
'blacklist': self._router.get_module_blacklist(),
15051512
'max_message_size': self.options.max_message_size,
15061513
'version': mitogen.__version__,
1514+
'pickle_whitelist_patterns': self.options.pickle_whitelist_patterns,
15071515
}
15081516

15091517
def get_preamble(self):

tests/call_function_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def test_bad_return_value(self):
8181
lambda: self.local.call(func_with_bad_return_value))
8282
self.assertEquals(
8383
exc.args[0],
84-
"cannot unpickle '%s'/'CrazyType'" % (__name__,),
84+
"cannot unpickle '%s'/'CrazyType' - try using `pickle_whitelist_patterns`" % (__name__,),
8585
)
8686

8787
def test_aborted_on_local_context_disconnect(self):

0 commit comments

Comments
 (0)