Permalink
Browse files

Implemented experimental JSON based sessions

  • Loading branch information...
1 parent d4415dd commit 4df3bf2058954624f9376fd16774a769299dc40a @mitsuhiko mitsuhiko committed Aug 11, 2012
Showing with 172 additions and 1 deletion.
  1. +9 −0 CHANGES
  2. +7 −0 docs/api.rst
  3. +51 −0 docs/upgrading.rst
  4. +79 −1 flask/sessions.py
  5. +26 −0 flask/testsuite/basic.py
View
@@ -3,6 +3,15 @@ Flask Changelog
Here you can see the full list of changes between each Flask release.
+Version 0.10
+------------
+
+Release date to be decided.
+
+- Changed default cookie serialization format from pickle to JSON to
+ limit the impact an attacker can do if the secret key leaks. See
+ :ref:`upgrading-to-010` for more information.
+
Version 0.9
-----------
View
@@ -215,6 +215,13 @@ implementation that Flask is using.
.. autoclass:: SecureCookieSessionInterface
:members:
+.. autoclass:: UpgradeSecureCookieSessionInterface
+
+.. autoclass:: SecureCookieSession
+ :members:
+
+.. autoclass:: UpgradeSecureCookieSession
+
.. autoclass:: NullSession
:members:
View
@@ -19,6 +19,57 @@ installation, make sure to pass it the ``-U`` parameter::
$ easy_install -U Flask
+.. _upgrading-to-010:
+
+Version 0.10
+------------
+
+The biggest change going from 0.9 to 0.10 is that the cookie serialization
+format changed from pickle to a specialized JSON format. This change has
+been done in order to avoid the damage an attacker can do if the secret
+key is leaked. When you upgrade you will notice two major changes: all
+sessions that were issued before the upgrade are invalidated and you can
+only store a limited amount of types in the session. There are two ways
+to avoid these problems on upgrading:
+
+Automatically Upgrade Sessions
+``````````````````````````````
+
+The first method is to allow pickle based sessions for a limited amount of
+time. This can be done by using the
+:class:`~flask.sessions.UpgradeSecureCookieSession` session
+implementation::
+
+ from flask import Flask
+ from flask.sessions import UpgradeSecureCookieSessionInterface
+
+ app = Flask(__name__)
+ app.session_interface = UpgradeSecureCookieSessionInterface
+
+For as long as this class is being used both pickle and json sessions are
+supported but changes are written in JSON format only.
+
+Revert to Pickle Sessions
+`````````````````````````
+
+You can also revert to pickle based sessions if you want::
+
+ import pickle
+ from flask import Flask
+ from flask.sessions import SecureCookieSession, \
+ SecureCookieSessionInterface
+
+ class PickleSessionInterface(SecureCookieSessionInterface):
+ class session_class(SecureCookieSession):
+ serialization_method = pickle
+
+ app = Flask(__name__)
+ app.session_interface = PickleSessionInterface
+
+If you want to continue to use pickle based data we strongly recommend
+switching to a server side session store however.
+
+
Version 0.9
-----------
View
@@ -10,8 +10,12 @@
:license: BSD, see LICENSE for more details.
"""
+import cPickle as pickle
from datetime import datetime
from werkzeug.contrib.securecookie import SecureCookie
+from werkzeug.http import http_date, parse_date
+from .helpers import json, _assert_have_json
+from . import Markup
class SessionMixin(object):
@@ -41,10 +45,74 @@ def _set_permanent(self, value):
modified = True
+class TaggedJSONSerializer(object):
+ """A customized JSON serializer that supports a few extra types that
+ we take for granted when serializing (tuples, markup objects, datetime).
+ """
+
+ def dumps(self, value):
+ if __debug__:
+ _assert_have_json()
+ def _tag(value):
+ if isinstance(value, tuple):
+ return {'##t': [_tag(x) for x in value]}
+ elif callable(getattr(value, '__html__', None)):
+ return {'##m': unicode(value.__html__())}
+ elif isinstance(value, list):
+ return [_tag(x) for x in value]
+ elif isinstance(value, datetime):
+ return {'##d': http_date(value)}
+ elif isinstance(value, dict):
+ return dict((k, _tag(v)) for k, v in value.iteritems())
+ return value
+ return json.dumps(_tag(value), separators=(',', ':'))
+
+ def loads(self, value):
+ if __debug__:
+ _assert_have_json()
+ def object_hook(obj):
+ if len(obj) != 1:
+ return obj
+ the_key, the_value = obj.iteritems().next()
+ if the_key == '##t':
+ return tuple(the_value)
+ elif the_key == '##m':
+ return Markup(the_value)
+ elif the_key == '##d':
+ return parse_date(the_value)
+ return obj
+ return json.loads(value, object_hook=object_hook)
+
+
+session_json_serializer = TaggedJSONSerializer()
+
+
class SecureCookieSession(SecureCookie, SessionMixin):
"""Expands the session with support for switching between permanent
- and non-permanent sessions.
+ and non-permanent sessions and changes the default pickle based
+ serialization format to a tagged json one.
+ """
+ serialization_method = session_json_serializer
+
+
+class _UpgradeSerializer(object):
+ def dumps(self, value):
+ return session_json_serializer.dumps(value)
+ def loads(self, value):
+ try:
+ return session_json_serializer.loads(value)
+ except Exception:
+ return pickle.loads(value)
+
+
+class UpgradeSecureCookieSession(SecureCookieSession):
+ """This cookie sesion implementation tries json first but will also
+ support pickle based session. This exists mainly to upgrade existing
+ pickle based users transparently to json.
+
+ .. versionadded:: 0.10
"""
+ serialization_method = _UpgradeSerializer()
class NullSession(SecureCookieSession):
@@ -203,3 +271,13 @@ def save_session(self, app, session, response):
session.save_cookie(response, app.session_cookie_name, path=path,
expires=expires, httponly=httponly,
secure=secure, domain=domain)
+
+
+class UpgradeSecureCookieSessionInterface(SecureCookieSessionInterface):
+ """This session interface works exactly like the regular one but uses
+ the :class:`UpgradeSecureCookieSession` classes to upgrade from pickle
+ sessions to JSON sessions.
+
+ .. versionadded:: 0.10
+ """
+ session_class = UpgradeSecureCookieSession
View
@@ -13,6 +13,7 @@
import re
import flask
+import pickle
import unittest
from datetime import datetime
from threading import Thread
@@ -297,6 +298,31 @@ def dump_session_contents():
self.assert_equal(c.get('/').data, 'None')
self.assert_equal(c.get('/').data, '42')
+ def test_session_special_types(self):
+ app = flask.Flask(__name__)
+ app.secret_key = 'development-key'
+ app.testing = True
+ now = datetime.utcnow().replace(microsecond=0)
+
+ @app.after_request
+ def modify_session(response):
+ flask.session['m'] = flask.Markup('Hello!')
+ flask.session['dt'] = now
+ flask.session['t'] = (1, 2, 3)
+ return response
+
+ @app.route('/')
+ def dump_session_contents():
+ return pickle.dumps(dict(flask.session))
+
+ c = app.test_client()
+ c.get('/')
+ rv = pickle.loads(c.get('/').data)
+ self.assert_equal(rv['m'], flask.Markup('Hello!'))
+ self.assert_equal(type(rv['m']), flask.Markup)
+ self.assert_equal(rv['dt'], now)
+ self.assert_equal(rv['t'], (1, 2, 3))
+
def test_flashes(self):
app = flask.Flask(__name__)
app.secret_key = 'testkey'

0 comments on commit 4df3bf2

Please sign in to comment.