diff --git a/CHANGES b/CHANGES index 6d0c4925..1948b5ab 100644 --- a/CHANGES +++ b/CHANGES @@ -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 ----------- diff --git a/docs/api.rst b/docs/api.rst index 8a7b5ce0..e808e771 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -215,6 +215,13 @@ implementation that Flask is using. .. autoclass:: SecureCookieSessionInterface :members: +.. autoclass:: UpgradeSecureCookieSessionInterface + +.. autoclass:: SecureCookieSession + :members: + +.. autoclass:: UpgradeSecureCookieSession + .. autoclass:: NullSession :members: diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 7226d60e..01393983 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -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 ----------- diff --git a/flask/sessions.py b/flask/sessions.py index 2795bb1f..75f4a614 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -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 @@ class SessionMixin(object): 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 @@ class SecureCookieSessionInterface(SessionInterface): 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 diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 388b5a8e..3d758b3a 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -13,6 +13,7 @@ from __future__ import with_statement import re import flask +import pickle import unittest from datetime import datetime from threading import Thread @@ -297,6 +298,31 @@ class BasicFunctionalityTestCase(FlaskTestCase): 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'