summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.rst13
-rw-r--r--src/pyramid/session.py218
-rw-r--r--tests/test_session.py173
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