diff options
| author | Michael Merickel <michael@merickel.org> | 2018-11-03 13:27:32 -0500 |
|---|---|---|
| committer | Michael Merickel <michael@merickel.org> | 2018-11-03 13:34:58 -0500 |
| commit | 02caee917f1b629467942ae3112d10e13d03202a (patch) | |
| tree | 4f433ad0811495ced4900055426f5d9ac3c658f5 | |
| parent | fc67869fb2732e715905614af3f9a69d48aed644 (diff) | |
| download | pyramid-02caee917f1b629467942ae3112d10e13d03202a.tar.gz pyramid-02caee917f1b629467942ae3112d10e13d03202a.tar.bz2 pyramid-02caee917f1b629467942ae3112d10e13d03202a.zip | |
remove UnencryptedCookieSessionFactoryConfig and signed_(de)serialize
| -rw-r--r-- | CHANGES.rst | 13 | ||||
| -rw-r--r-- | src/pyramid/session.py | 218 | ||||
| -rw-r--r-- | tests/test_session.py | 173 |
3 files changed, 13 insertions, 391 deletions
diff --git a/CHANGES.rst b/CHANGES.rst index f847cec7a..dfea7afa9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -39,6 +39,19 @@ Backward Incompatibilities matching that was not compliant with the RFC. See https://github.com/Pylons/pyramid/pull/3411 +- Removed ``pyramid.session.UnencryptedCookieSessionFactoryConfig``. This + session factory was replaced with + ``pyramid.session.SignedCookieSessionFactory`` in Pyramid 1.5 and has been + deprecated since then. + See https://github.com/Pylons/pyramid/pull/3412 + +- Removed ``pyramid.session.signed_serialize``, and + ``pyramid.session.signed_deserialize``. These methods were only used by + the now-removed ``pyramid.session.UnencryptedCookieSessionFactoryConfig`` + and were coupled to the vulnerable pickle serialization format which could + lead to remove code execution if the secret key is compromised. + See https://github.com/Pylons/pyramid/pull/3412 + Documentation Changes --------------------- diff --git a/src/pyramid/session.py b/src/pyramid/session.py index 9d4ef6dbb..d26344aea 100644 --- a/src/pyramid/session.py +++ b/src/pyramid/session.py @@ -1,7 +1,4 @@ -import base64 import binascii -import hashlib -import hmac import os import time import warnings @@ -15,7 +12,6 @@ from pyramid.compat import pickle, PY2, text_, bytes_, native_ from pyramid.csrf import check_csrf_origin, check_csrf_token from pyramid.interfaces import ISession -from pyramid.util import strings_differ def manage_accessed(wrapped): @@ -46,98 +42,6 @@ def manage_changed(wrapped): return changed -def signed_serialize(data, secret): - """ Serialize any pickleable structure (``data``) and sign it - using the ``secret`` (must be a string). Return the - serialization, which includes the signature as its first 40 bytes. - The ``signed_deserialize`` method will deserialize such a value. - - This function is useful for creating signed cookies. For example: - - .. code-block:: python - - cookieval = signed_serialize({'a':1}, 'secret') - response.set_cookie('signed_cookie', cookieval) - - .. deprecated:: 1.10 - - This function will be removed in :app:`Pyramid` 2.0. It is using - pickle-based serialization, which is considered vulnerable to remote - code execution attacks and will no longer be used by the default - session factories at that time. - - """ - pickled = pickle.dumps(data, pickle.HIGHEST_PROTOCOL) - try: - # bw-compat with pyramid <= 1.5b1 where latin1 is the default - secret = bytes_(secret) - except UnicodeEncodeError: - secret = bytes_(secret, 'utf-8') - sig = hmac.new(secret, pickled, hashlib.sha1).hexdigest() - return sig + native_(base64.b64encode(pickled)) - - -deprecated( - 'signed_serialize', - 'This function will be removed in Pyramid 2.0. It is using pickle-based ' - 'serialization, which is considered vulnerable to remote code execution ' - 'attacks.', -) - - -def signed_deserialize(serialized, secret, hmac=hmac): - """ Deserialize the value returned from ``signed_serialize``. If - the value cannot be deserialized for any reason, a - :exc:`ValueError` exception will be raised. - - This function is useful for deserializing a signed cookie value - created by ``signed_serialize``. For example: - - .. code-block:: python - - cookieval = request.cookies['signed_cookie'] - data = signed_deserialize(cookieval, 'secret') - - .. deprecated:: 1.10 - - This function will be removed in :app:`Pyramid` 2.0. It is using - pickle-based serialization, which is considered vulnerable to remote - code execution attacks and will no longer be used by the default - session factories at that time. - """ - # hmac parameterized only for unit tests - try: - input_sig, pickled = ( - bytes_(serialized[:40]), - base64.b64decode(bytes_(serialized[40:])), - ) - except (binascii.Error, TypeError) as e: - # Badly formed data can make base64 die - raise ValueError('Badly formed base64 data: %s' % e) - - try: - # bw-compat with pyramid <= 1.5b1 where latin1 is the default - secret = bytes_(secret) - except UnicodeEncodeError: - secret = bytes_(secret, 'utf-8') - sig = bytes_(hmac.new(secret, pickled, hashlib.sha1).hexdigest()) - - # Avoid timing attacks (see - # http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf) - if strings_differ(sig, input_sig): - raise ValueError('Invalid signature') - - return pickle.loads(pickled) - - -deprecated( - 'signed_deserialize', - 'This function will be removed in Pyramid 2.0. It is using pickle-based ' - 'serialization, which is considered vulnerable to remote code execution ' - 'attacks.', -) - - class PickleSerializer(object): """ A serializer that uses the pickle protocol to dump Python data to bytes. @@ -429,128 +333,6 @@ def BaseCookieSessionFactory( return CookieSession -def UnencryptedCookieSessionFactoryConfig( - secret, - timeout=1200, - cookie_name='session', - cookie_max_age=None, - cookie_path='/', - cookie_domain=None, - cookie_secure=False, - cookie_httponly=False, - cookie_samesite='Lax', - cookie_on_exception=True, - signed_serialize=signed_serialize, - signed_deserialize=signed_deserialize, -): - """ - .. deprecated:: 1.5 - Use :func:`pyramid.session.SignedCookieSessionFactory` instead. - Caveat: Cookies generated using ``SignedCookieSessionFactory`` are not - compatible with cookies generated using - ``UnencryptedCookieSessionFactory``, so existing user session data - will be destroyed if you switch to it. - - Configure a :term:`session factory` which will provide unencrypted - (but signed) cookie-based sessions. The return value of this - function is a :term:`session factory`, which may be provided as - the ``session_factory`` argument of a - :class:`pyramid.config.Configurator` constructor, or used - as the ``session_factory`` argument of the - :meth:`pyramid.config.Configurator.set_session_factory` - method. - - The session factory returned by this function will create sessions - which are limited to storing fewer than 4000 bytes of data (as the - payload must fit into a single cookie). - - Parameters: - - ``secret`` - A string which is used to sign the cookie. - - ``timeout`` - A number of seconds of inactivity before a session times out. - - ``cookie_name`` - The name of the cookie used for sessioning. - - ``cookie_max_age`` - The maximum age of the cookie used for sessioning (in seconds). - Default: ``None`` (browser scope). - - ``cookie_path`` - The path used for the session cookie. - - ``cookie_domain`` - The domain used for the session cookie. Default: ``None`` (no domain). - - ``cookie_secure`` - The 'secure' flag of the session cookie. - - ``cookie_httponly`` - The 'httpOnly' flag of the session cookie. - - ``cookie_samesite`` - The 'samesite' option of the session cookie. Set the value to ``None`` - to turn off the samesite option. Default: ``'Lax'``. - - ``cookie_on_exception`` - If ``True``, set a session cookie even if an exception occurs - while rendering a view. - - ``signed_serialize`` - A callable which takes more or less arbitrary Python data structure and - a secret and returns a signed serialization in bytes. - Default: ``signed_serialize`` (using pickle). - - ``signed_deserialize`` - A callable which takes a signed and serialized data structure in bytes - and a secret and returns the original data structure if the signature - is valid. Default: ``signed_deserialize`` (using pickle). - - .. versionchanged: 1.10 - - Added the ``samesite`` option and made the default ``'Lax'``. - """ - - class SerializerWrapper(object): - def __init__(self, secret): - self.secret = secret - - def loads(self, bstruct): - return signed_deserialize(bstruct, secret) - - def dumps(self, appstruct): - return signed_serialize(appstruct, secret) - - serializer = SerializerWrapper(secret) - - return BaseCookieSessionFactory( - serializer, - cookie_name=cookie_name, - max_age=cookie_max_age, - path=cookie_path, - domain=cookie_domain, - secure=cookie_secure, - httponly=cookie_httponly, - samesite=cookie_samesite, - timeout=timeout, - reissue_time=0, # to keep session.accessed == session.renewed - set_on_exception=cookie_on_exception, - ) - - -deprecated( - 'UnencryptedCookieSessionFactoryConfig', - 'The UnencryptedCookieSessionFactoryConfig callable is deprecated as of ' - 'Pyramid 1.5. Use ``pyramid.session.SignedCookieSessionFactory`` instead.' - ' Caveat: Cookies generated using SignedCookieSessionFactory are not ' - 'compatible with cookies generated using UnencryptedCookieSessionFactory, ' - 'so existing user session data will be destroyed if you switch to it.', -) - - def SignedCookieSessionFactory( secret, cookie_name='session', diff --git a/tests/test_session.py b/tests/test_session.py index 05c257e73..6f93864a5 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -524,94 +524,6 @@ class TestSignedCookieSession(SharedCookieSessionTests, unittest.TestCase): self.assertEqual(session, {}) -class TestUnencryptedCookieSession( - SharedCookieSessionTests, unittest.TestCase -): - def setUp(self): - super(TestUnencryptedCookieSession, self).setUp() - from zope.deprecation import __show__ - - __show__.off() - - def tearDown(self): - super(TestUnencryptedCookieSession, self).tearDown() - from zope.deprecation import __show__ - - __show__.on() - - def _makeOne(self, request, **kw): - from pyramid.session import UnencryptedCookieSessionFactoryConfig - - self._rename_cookie_var(kw, 'path', 'cookie_path') - self._rename_cookie_var(kw, 'domain', 'cookie_domain') - self._rename_cookie_var(kw, 'secure', 'cookie_secure') - self._rename_cookie_var(kw, 'httponly', 'cookie_httponly') - self._rename_cookie_var(kw, 'set_on_exception', 'cookie_on_exception') - return UnencryptedCookieSessionFactoryConfig('secret', **kw)(request) - - def _rename_cookie_var(self, kw, src, dest): - if src in kw: - kw.setdefault(dest, kw.pop(src)) - - def _serialize(self, value): - from pyramid.compat import bytes_ - from pyramid.session import signed_serialize - - return bytes_(signed_serialize(value, 'secret')) - - def test_serialize_option(self): - from pyramid.response import Response - - secret = 'secret' - request = testing.DummyRequest() - session = self._makeOne( - request, signed_serialize=dummy_signed_serialize - ) - session['key'] = 'value' - response = Response() - self.assertEqual(session._set_cookie(response), True) - cookie = response.headerlist[-1][1] - expected_cookieval = dummy_signed_serialize( - (session.accessed, session.created, {'key': 'value'}), secret - ) - response = Response() - response.set_cookie('session', expected_cookieval, samesite='Lax') - expected_cookie = response.headerlist[-1][1] - self.assertEqual(cookie, expected_cookie) - - def test_deserialize_option(self): - import time - - secret = 'secret' - request = testing.DummyRequest() - accessed = time.time() - state = {'key': 'value'} - cookieval = dummy_signed_serialize((accessed, accessed, state), secret) - request.cookies['session'] = cookieval - session = self._makeOne( - request, signed_deserialize=dummy_signed_deserialize - ) - self.assertEqual(dict(session), state) - - -def dummy_signed_serialize(data, secret): - import base64 - from pyramid.compat import pickle, bytes_ - - pickled = pickle.dumps(data) - return base64.b64encode(bytes_(secret)) + base64.b64encode(pickled) - - -def dummy_signed_deserialize(serialized, secret): - import base64 - from pyramid.compat import pickle, bytes_ - - serialized_data = base64.b64decode( - serialized[len(base64.b64encode(bytes_(secret))) :] - ) - return pickle.loads(serialized_data) - - class Test_manage_accessed(unittest.TestCase): def _makeOne(self, wrapped): from pyramid.session import manage_accessed @@ -669,91 +581,6 @@ class Test_manage_changed(unittest.TestCase): self.assertTrue(session._dirty) -def serialize(data, secret): - import hmac - import base64 - from hashlib import sha1 - from pyramid.compat import bytes_ - from pyramid.compat import native_ - from pyramid.compat import pickle - - pickled = pickle.dumps(data, pickle.HIGHEST_PROTOCOL) - sig = hmac.new(bytes_(secret, 'utf-8'), pickled, sha1).hexdigest() - return sig + native_(base64.b64encode(pickled)) - - -class Test_signed_serialize(unittest.TestCase): - def _callFUT(self, data, secret): - from pyramid.session import signed_serialize - - return signed_serialize(data, secret) - - def test_it(self): - expected = serialize('123', 'secret') - result = self._callFUT('123', 'secret') - self.assertEqual(result, expected) - - def test_it_with_highorder_secret(self): - secret = b'\xce\xb1\xce\xb2\xce\xb3\xce\xb4'.decode('utf-8') - expected = serialize('123', secret) - result = self._callFUT('123', secret) - self.assertEqual(result, expected) - - def test_it_with_latin1_secret(self): - secret = b'La Pe\xc3\xb1a' - expected = serialize('123', secret) - result = self._callFUT('123', secret.decode('latin-1')) - self.assertEqual(result, expected) - - -class Test_signed_deserialize(unittest.TestCase): - def _callFUT(self, serialized, secret, hmac=None): - if hmac is None: - import hmac - from pyramid.session import signed_deserialize - - return signed_deserialize(serialized, secret, hmac=hmac) - - def test_it(self): - serialized = serialize('123', 'secret') - result = self._callFUT(serialized, 'secret') - self.assertEqual(result, '123') - - def test_invalid_bits(self): - serialized = serialize('123', 'secret') - self.assertRaises(ValueError, self._callFUT, serialized, 'seekrit') - - def test_invalid_len(self): - class hmac(object): - def new(self, *arg): - return self - - def hexdigest(self): - return '1234' - - serialized = serialize('123', 'secret123') - self.assertRaises( - ValueError, self._callFUT, serialized, 'secret', hmac=hmac() - ) - - def test_it_bad_encoding(self): - serialized = 'bad' + serialize('123', 'secret') - self.assertRaises(ValueError, self._callFUT, serialized, 'secret') - - def test_it_with_highorder_secret(self): - secret = b'\xce\xb1\xce\xb2\xce\xb3\xce\xb4'.decode('utf-8') - serialized = serialize('123', secret) - result = self._callFUT(serialized, secret) - self.assertEqual(result, '123') - - # bwcompat with pyramid <= 1.5b1 where latin1 is the default - def test_it_with_latin1_secret(self): - secret = b'La Pe\xc3\xb1a' - serialized = serialize('123', secret) - result = self._callFUT(serialized, secret.decode('latin-1')) - self.assertEqual(result, '123') - - class TestPickleSerializer(unittest.TestCase): def _makeOne(self): from pyramid.session import PickleSerializer |
