Skip to content

Commit

Permalink
[1.5.x] Fixed #20922 -- Allowed customizing the serializer used by co…
Browse files Browse the repository at this point in the history
…ntrib.sessions

Added settings.SESSION_SERIALIZER which is the import path of a serializer
to use for sessions.

Thanks apollo13, carljm, shaib, akaariai, charettes, and dstufft for reviews.

Backport of b0ce6fe from master
  • Loading branch information
timgraham committed Aug 22, 2013
1 parent 1b23604 commit 616a4d3
Show file tree
Hide file tree
Showing 15 changed files with 253 additions and 79 deletions.
1 change: 1 addition & 0 deletions django/conf/global_settings.py
Expand Up @@ -467,6 +467,7 @@
SESSION_EXPIRE_AT_BROWSER_CLOSE = False # Whether a user's session cookie expires when the Web browser is closed.
SESSION_ENGINE = 'django.contrib.sessions.backends.db' # The module to store session data
SESSION_FILE_PATH = None # Directory to store session files if using the file session module. If None, the backend will use a sensible default.
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer' # class to serialize session data

#########
# CACHE #
Expand Down
2 changes: 2 additions & 0 deletions django/conf/project_template/project_name/settings.py
Expand Up @@ -126,6 +126,8 @@
# 'django.contrib.admindocs',
)

SESSION_SERIALIZER = 'django.contrib.sessions.serializers.JSONSerializer'

# A sample logging configuration. The only tangible logging
# performed by this configuration is to send an email to
# the site admins on every HTTP 500 error when DEBUG=False.
Expand Down
17 changes: 15 additions & 2 deletions django/contrib/messages/storage/session.py
@@ -1,4 +1,8 @@
import json

from django.contrib.messages.storage.base import BaseStorage
from django.contrib.messages.storage.cookie import MessageEncoder, MessageDecoder
from django.utils import six


class SessionStorage(BaseStorage):
Expand All @@ -20,14 +24,23 @@ def _get(self, *args, **kwargs):
always stores everything it is given, so return True for the
all_retrieved flag.
"""
return self.request.session.get(self.session_key), True
return self.deserialize_messages(self.request.session.get(self.session_key)), True

def _store(self, messages, response, *args, **kwargs):
"""
Stores a list of messages to the request's session.
"""
if messages:
self.request.session[self.session_key] = messages
self.request.session[self.session_key] = self.serialize_messages(messages)
else:
self.request.session.pop(self.session_key, None)
return []

def serialize_messages(self, messages):
encoder = MessageEncoder(separators=(',', ':'))
return encoder.encode(messages)

def deserialize_messages(self, data):
if data and isinstance(data, six.string_types):
return json.loads(data, cls=MessageDecoder)
return data
1 change: 1 addition & 0 deletions django/contrib/messages/tests/base.py
Expand Up @@ -61,6 +61,7 @@ def setUp(self):
MESSAGE_TAGS = '',
MESSAGE_STORAGE = '%s.%s' % (self.storage_class.__module__,
self.storage_class.__name__),
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.JSONSerializer',
)
self.settings_override.enable()

Expand Down
4 changes: 2 additions & 2 deletions django/contrib/messages/tests/session.py
Expand Up @@ -10,13 +10,13 @@ def set_session_data(storage, messages):
Sets the messages into the backend request's session and remove the
backend's loaded data cache.
"""
storage.request.session[storage.session_key] = messages
storage.request.session[storage.session_key] = storage.serialize_messages(messages)
if hasattr(storage, '_loaded_data'):
del storage._loaded_data


def stored_session_messages_count(storage):
data = storage.request.session.get(storage.session_key, [])
data = storage.deserialize_messages(storage.request.session.get(storage.session_key, []))
return len(data)


Expand Down
25 changes: 12 additions & 13 deletions django/contrib/sessions/backends/base.py
Expand Up @@ -2,10 +2,6 @@

import base64
from datetime import datetime, timedelta
try:
from django.utils.six.moves import cPickle as pickle
except ImportError:
import pickle
import string

from django.conf import settings
Expand All @@ -15,6 +11,7 @@
from django.utils.crypto import salted_hmac
from django.utils import timezone
from django.utils.encoding import force_bytes
from django.utils.module_loading import import_by_path

# session_key should not be case sensitive because some backends can store it
# on case insensitive file systems.
Expand All @@ -38,6 +35,7 @@ def __init__(self, session_key=None):
self._session_key = session_key
self.accessed = False
self.modified = False
self.serializer = import_by_path(settings.SESSION_SERIALIZER)

def __contains__(self, key):
return key in self._session
Expand Down Expand Up @@ -82,24 +80,25 @@ def _hash(self, value):
return salted_hmac(key_salt, value).hexdigest()

def encode(self, session_dict):
"Returns the given session dictionary pickled and encoded as a string."
pickled = pickle.dumps(session_dict, pickle.HIGHEST_PROTOCOL)
hash = self._hash(pickled)
return base64.b64encode(hash.encode() + b":" + pickled).decode('ascii')
"Returns the given session dictionary serialized and encoded as a string."
serialized = self.serializer().dumps(session_dict)
hash = self._hash(serialized)
return base64.b64encode(hash.encode() + b":" + serialized).decode('ascii')

def decode(self, session_data):
encoded_data = base64.b64decode(force_bytes(session_data))
try:
# could produce ValueError if there is no ':'
hash, pickled = encoded_data.split(b':', 1)
expected_hash = self._hash(pickled)
hash, serialized = encoded_data.split(b':', 1)
expected_hash = self._hash(serialized)
if not constant_time_compare(hash.decode(), expected_hash):
raise SuspiciousOperation("Session data corrupted")
else:
return pickle.loads(pickled)
return self.serializer().loads(serialized)
except Exception:
# ValueError, SuspiciousOperation, unpickling exceptions. If any of
# these happen, just return an empty dictionary (an empty session).
# ValueError, SuspiciousOperation, deserialization exceptions. If
# any of these happen, just return an empty dictionary (an empty
# session).
return {}

def update(self, dict_):
Expand Down
21 changes: 2 additions & 19 deletions django/contrib/sessions/backends/signed_cookies.py
@@ -1,26 +1,9 @@
try:
from django.utils.six.moves import cPickle as pickle
except ImportError:
import pickle

from django.conf import settings
from django.core import signing

from django.contrib.sessions.backends.base import SessionBase


class PickleSerializer(object):
"""
Simple wrapper around pickle to be used in signing.dumps and
signing.loads.
"""
def dumps(self, obj):
return pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)

def loads(self, data):
return pickle.loads(data)


class SessionStore(SessionBase):

def load(self):
Expand All @@ -31,7 +14,7 @@ def load(self):
"""
try:
return signing.loads(self.session_key,
serializer=PickleSerializer,
serializer=self.serializer,
# This doesn't handle non-default expiry dates, see #19201
max_age=settings.SESSION_COOKIE_AGE,
salt='django.contrib.sessions.backends.signed_cookies')
Expand Down Expand Up @@ -91,7 +74,7 @@ def _get_session_key(self):
session_cache = getattr(self, '_session_cache', {})
return signing.dumps(session_cache, compress=True,
salt='django.contrib.sessions.backends.signed_cookies',
serializer=PickleSerializer)
serializer=self.serializer)

@classmethod
def clear_expired(cls):
Expand Down
2 changes: 1 addition & 1 deletion django/contrib/sessions/models.py
Expand Up @@ -5,7 +5,7 @@
class SessionManager(models.Manager):
def encode(self, session_dict):
"""
Returns the given session dictionary pickled and encoded as a string.
Returns the given session dictionary serialized and encoded as a string.
"""
return SessionStore().encode(session_dict)

Expand Down
20 changes: 20 additions & 0 deletions django/contrib/sessions/serializers.py
@@ -0,0 +1,20 @@
from django.core.signing import JSONSerializer as BaseJSONSerializer
try:
from django.utils.six.moves import cPickle as pickle
except ImportError:
import pickle


class PickleSerializer(object):
"""
Simple wrapper around pickle to be used in signing.dumps and
signing.loads.
"""
def dumps(self, obj):
return pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)

def loads(self, data):
return pickle.loads(data)


JSONSerializer = BaseJSONSerializer
34 changes: 19 additions & 15 deletions django/contrib/sessions/tests.py
Expand Up @@ -273,21 +273,25 @@ def test_decode(self):
self.assertEqual(self.session.decode(encoded), data)

def test_actual_expiry(self):
# Regression test for #19200
old_session_key = None
new_session_key = None
try:
self.session['foo'] = 'bar'
self.session.set_expiry(-timedelta(seconds=10))
self.session.save()
old_session_key = self.session.session_key
# With an expiry date in the past, the session expires instantly.
new_session = self.backend(self.session.session_key)
new_session_key = new_session.session_key
self.assertNotIn('foo', new_session)
finally:
self.session.delete(old_session_key)
self.session.delete(new_session_key)
# this doesn't work with JSONSerializer (serializing timedelta)
with override_settings(SESSION_SERIALIZER='django.contrib.sessions.serializers.PickleSerializer'):
self.session = self.backend() # reinitialize after overriding settings

# Regression test for #19200
old_session_key = None
new_session_key = None
try:
self.session['foo'] = 'bar'
self.session.set_expiry(-timedelta(seconds=10))
self.session.save()
old_session_key = self.session.session_key
# With an expiry date in the past, the session expires instantly.
new_session = self.backend(self.session.session_key)
new_session_key = new_session.session_key
self.assertNotIn('foo', new_session)
finally:
self.session.delete(old_session_key)
self.session.delete(new_session_key)


class DatabaseSessionTests(SessionTestsMixin, TestCase):
Expand Down
29 changes: 29 additions & 0 deletions django/utils/module_loading.py
Expand Up @@ -2,6 +2,35 @@
import os
import sys

from django.core.exceptions import ImproperlyConfigured
from django.utils import six
from django.utils.importlib import import_module


def import_by_path(dotted_path, error_prefix=''):
"""
Import a dotted module path and return the attribute/class designated by the
last name in the path. Raise ImproperlyConfigured if something goes wrong.
"""
try:
module_path, class_name = dotted_path.rsplit('.', 1)
except ValueError:
raise ImproperlyConfigured("%s%s doesn't look like a module path" % (
error_prefix, dotted_path))
try:
module = import_module(module_path)
except ImportError as e:
msg = '%sError importing module %s: "%s"' % (
error_prefix, module_path, e)
six.reraise(ImproperlyConfigured, ImproperlyConfigured(msg),
sys.exc_info()[2])
try:
attr = getattr(module, class_name)
except AttributeError:
raise ImproperlyConfigured('%sModule "%s" does not define a "%s" attribute/class' % (
error_prefix, module_path, class_name))
return attr


def module_has_submodule(package, module_name):
"""See if 'module' is in 'package'."""
Expand Down
28 changes: 27 additions & 1 deletion docs/ref/settings.txt
Expand Up @@ -1444,6 +1444,8 @@ Sets the minimum message level that will be recorded by the messages
framework. See the :doc:`messages documentation </ref/contrib/messages>` for
more details.

.. setting:: MESSAGE_STORAGE

MESSAGE_STORAGE
---------------

Expand Down Expand Up @@ -1817,7 +1819,7 @@ SESSION_ENGINE

Default: ``django.contrib.sessions.backends.db``

Controls where Django stores session data. Valid values are:
Controls where Django stores session data. Included engines are:

* ``'django.contrib.sessions.backends.db'``
* ``'django.contrib.sessions.backends.file'``
Expand Down Expand Up @@ -1859,6 +1861,30 @@ Default: ``False``
Whether to save the session data on every request. See
:doc:`/topics/http/sessions`.

.. setting:: SESSION_SERIALIZER

SESSION_SERIALIZER
------------------

.. versionadded:: 1.5.3

Default: ``'django.contrib.sessions.serializers.PickleSerializer'``

Full import path of a serializer class to use for serializing session data.
Included serializers are:

* ``'django.contrib.sessions.serializers.PickleSerializer'``
* ``'django.contrib.sessions.serializers.JSONSerializer'``

See :ref:`session_serialization` for details, including a warning regarding
possible remote code execution when using
:class:`~django.contrib.sessions.serializers.PickleSerializer`.

In Django 1.5.3, the default in newly created projects using
:djadmin:`django-admin.py startproject <startproject>` is
:class:`django.contrib.sessions.serializers.JSONSerializer`, and the global
default will switch to this class in Django 1.6.

.. setting:: SHORT_DATE_FORMAT

SHORT_DATE_FORMAT
Expand Down

0 comments on commit 616a4d3

Please sign in to comment.